/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.core.query.lucene;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.Weight;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.io.IOException;
import java.util.BitSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Implements a lucene <code>Query</code> which filters a sub query by checking
* whether the nodes selected by that sub query are descendants or self of
* nodes selected by a context query.
*/
@SuppressWarnings("serial")
class DescendantSelfAxisQuery extends Query implements JackrabbitQuery {
/**
* The logger instance for this class.
*/
private static final Logger log = LoggerFactory.getLogger(DescendantSelfAxisQuery.class);
/**
* The context query
*/
private final Query contextQuery;
/**
* The scorer of the context query
*/
private Scorer contextScorer;
/**
* The sub query to filter
*/
private final Query subQuery;
/**
* The minimal levels required between context and sub nodes for a sub node
* to match.
*/
private final int minLevels;
/**
* The scorer of the sub query to filter
*/
private Scorer subScorer;
/**
* Creates a new <code>DescendantSelfAxisQuery</code> based on a
* <code>context</code> and matches all descendants of the context nodes.
* Whether the context nodes match as well is controlled by
* <code>includeSelf</code>.
*
* @param context the context for this query.
* @param includeSelf if <code>true</code> this query acts like a
* descendant-or-self axis. If <code>false</code> this
* query acts like a descendant axis.
*/
public DescendantSelfAxisQuery(Query context, boolean includeSelf) {
this(context, new MatchAllDocsQuery(), includeSelf);
}
/**
* Creates a new <code>DescendantSelfAxisQuery</code> based on a
* <code>context</code> query and filtering the <code>sub</code> query.
*
* @param context the context for this query.
* @param sub the sub query.
*/
public DescendantSelfAxisQuery(Query context, Query sub) {
this(context, sub, true);
}
/**
* Creates a new <code>DescendantSelfAxisQuery</code> based on a
* <code>context</code> query and filtering the <code>sub</code> query.
*
* @param context the context for this query.
* @param sub the sub query.
* @param includeSelf if <code>true</code> this query acts like a
* descendant-or-self axis. If <code>false</code> this query acts like
* a descendant axis.
*/
public DescendantSelfAxisQuery(Query context, Query sub, boolean includeSelf) {
this(context, sub, includeSelf ? 0 : 1);
}
/**
* Creates a new <code>DescendantSelfAxisQuery</code> based on a
* <code>context</code> query and filtering the <code>sub</code> query.
*
* @param context the context for this query.
* @param sub the sub query.
* @param minLevels the minimal levels required between context and sub
* nodes for a sub node to match.
*/
public DescendantSelfAxisQuery(Query context, Query sub, int minLevels) {
this.contextQuery = context;
this.subQuery = sub;
this.minLevels = minLevels;
}
/**
* @return the context query of this <code>DescendantSelfAxisQuery</code>.
*/
Query getContextQuery() {
return contextQuery;
}
/**
* @return <code>true</code> if the sub query of this <code>DescendantSelfAxisQuery</code>
* matches all nodes.
*/
boolean subQueryMatchesAll() {
return subQuery instanceof MatchAllDocsQuery;
}
/**
* Returns the minimal levels required between context and sub nodes for a
* sub node to match.
* <ul>
* <li><code>0</code>: a sub node <code>S</code> matches if it is a context
* node or one of the ancestors of <code>S</code> is a context node.</li>
* <li><code>1</code>: a sub node <code>S</code> matches if one of the
* ancestors of <code>S</code> is a context node.</li>
* <li><code>n</code>: a sub node <code>S</code> matches if
* <code>S.getAncestor(S.getDepth() - n)</code> is a context node.</li>
* </ul>
*
* @return the minimal levels required between context and sub nodes for a
* sub node to match.
*/
int getMinLevels() {
return minLevels;
}
/**
* Creates a <code>Weight</code> instance for this query.
*
* @param searcher the <code>Searcher</code> instance to use.
* @return a <code>DescendantSelfAxisWeight</code>.
*/
public Weight createWeight(Searcher searcher) {
return new DescendantSelfAxisWeight(searcher);
}
/**
* {@inheritDoc}
*/
public String toString(String field) {
StringBuffer sb = new StringBuffer();
sb.append("DescendantSelfAxisQuery(");
sb.append(contextQuery);
sb.append(", ");
sb.append(subQuery);
sb.append(", ");
sb.append(minLevels);
sb.append(")");
return sb.toString();
}
/**
* {@inheritDoc}
*/
public void extractTerms(Set<Term> terms) {
contextQuery.extractTerms(terms);
subQuery.extractTerms(terms);
}
/**
* {@inheritDoc}
*/
public Query rewrite(IndexReader reader) throws IOException {
Query cQuery = contextQuery.rewrite(reader);
Query sQuery = subQuery.rewrite(reader);
if (contextQuery instanceof DescendantSelfAxisQuery) {
DescendantSelfAxisQuery dsaq = (DescendantSelfAxisQuery) contextQuery;
if (dsaq.subQueryMatchesAll()) {
return new DescendantSelfAxisQuery(dsaq.getContextQuery(),
sQuery, dsaq.getMinLevels() + getMinLevels()).rewrite(reader);
}
}
if (cQuery == contextQuery && sQuery == subQuery) {
return this;
} else {
return new DescendantSelfAxisQuery(cQuery, sQuery, minLevels);
}
}
//------------------------< JackrabbitQuery >-------------------------------
/**
* {@inheritDoc}
*/
public QueryHits execute(final JackrabbitIndexSearcher searcher,
final SessionImpl session,
final Sort sort) throws IOException {
if (sort.getSort().length == 0 && subQueryMatchesAll()) {
// maps path String to ScoreNode
Map<String, ScoreNode> startingPoints = new TreeMap<String, ScoreNode>();
QueryHits result = searcher.evaluate(getContextQuery());
try {
// minLevels 0 and 1 are handled with a series of
// NodeTraversingQueryHits directly on result. For minLevels >= 2
// intermediate ChildNodesQueryHits are required.
for (int i = 2; i <= getMinLevels(); i++) {
result = new ChildNodesQueryHits(result, session);
}
ScoreNode sn;
while ((sn = result.nextScoreNode()) != null) {
NodeId id = sn.getNodeId();
try {
Node node = session.getNodeById(id);
startingPoints.put(node.getPath(), sn);
} catch (ItemNotFoundException e) {
// JCR-3001 access denied to score node, will just skip it
log.warn("Access denied to node id {}.", id);
} catch (RepositoryException e) {
throw Util.createIOException(e);
}
}
} finally {
result.close();
}
// prune overlapping starting points
String previousPath = null;
for (Iterator<String> it = startingPoints.keySet().iterator(); it.hasNext(); ) {
String path = it.next();
// if the previous path is a prefix of this path then the
// current path is obsolete
if (previousPath != null && path.startsWith(previousPath)) {
it.remove();
} else {
previousPath = path;
}
}
final Iterator<ScoreNode> scoreNodes = startingPoints.values().iterator();
return new AbstractQueryHits() {
private NodeTraversingQueryHits currentTraversal;
{
fetchNextTraversal();
}
public void close() throws IOException {
if (currentTraversal != null) {
currentTraversal.close();
}
}
public ScoreNode nextScoreNode() throws IOException {
while (currentTraversal != null) {
ScoreNode sn = currentTraversal.nextScoreNode();
if (sn != null) {
return sn;
} else {
fetchNextTraversal();
}
}
// if we get here there are no more score nodes
return null;
}
private void fetchNextTraversal() throws IOException {
if (currentTraversal != null) {
currentTraversal.close();
}
currentTraversal = null;
// We only need one node, but because of the acls, we'll
// iterate until we find a good one
while (scoreNodes.hasNext()) {
ScoreNode sn = scoreNodes.next();
NodeId id = sn.getNodeId();
try {
Node node = session.getNodeById(id);
currentTraversal = new NodeTraversingQueryHits(
node, getMinLevels() == 0);
break;
} catch (ItemNotFoundException e) {
// JCR-3001 node access denied, will just skip it
log.warn("Access denied to node id {}.", id);
} catch (RepositoryException e) {
throw Util.createIOException(e);
}
}
}
};
} else {
return null;
}
}
//--------------------< DescendantSelfAxisWeight >--------------------------
/**
* The <code>Weight</code> implementation for this
* <code>DescendantSelfAxisWeight</code>.
*/
private class DescendantSelfAxisWeight extends Weight {
/**
* The searcher in use
*/
private final Searcher searcher;
/**
* Creates a new <code>DescendantSelfAxisWeight</code> instance using
* <code>searcher</code>.
*
* @param searcher a <code>Searcher</code> instance.
*/
private DescendantSelfAxisWeight(Searcher searcher) {
this.searcher = searcher;
}
//-----------------------------< Weight >-------------------------------
/**
* Returns this <code>DescendantSelfAxisQuery</code>.
*
* @return this <code>DescendantSelfAxisQuery</code>.
*/
public Query getQuery() {
return DescendantSelfAxisQuery.this;
}
/**
* {@inheritDoc}
*/
public float getValue() {
return 1.0f;
}
/**
* {@inheritDoc}
*/
public float sumOfSquaredWeights() throws IOException {
return 1.0f;
}
/**
* {@inheritDoc}
*/
public void normalize(float norm) {
}
/**
* Creates a scorer for this <code>DescendantSelfAxisScorer</code>.
*
* @param reader a reader for accessing the index.
* @return a <code>DescendantSelfAxisScorer</code>.
* @throws IOException if an error occurs while reading from the index.
*/
public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder,
boolean topScorer) throws IOException {
contextScorer = searcher.createNormalizedWeight(contextQuery).scorer(reader, scoreDocsInOrder, false);
subScorer = searcher.createNormalizedWeight(subQuery).scorer(reader, scoreDocsInOrder, false);
HierarchyResolver resolver = (HierarchyResolver) reader;
return new DescendantSelfAxisScorer(searcher.getSimilarity(), reader, resolver);
}
/**
* {@inheritDoc}
*/
public Explanation explain(IndexReader reader, int doc) throws IOException {
return new Explanation();
}
}
//----------------------< DescendantSelfAxisScorer >---------------------------------
/**
* Implements a <code>Scorer</code> for this
* <code>DescendantSelfAxisQuery</code>.
*/
private class DescendantSelfAxisScorer extends Scorer {
/**
* The <code>HierarchyResolver</code> of the index.
*/
private final HierarchyResolver hResolver;
/**
* BitSet storing the id's of selected documents
*/
private final BitSet contextHits;
/**
* Set <code>true</code> once the context hits have been calculated.
*/
private boolean contextHitsCalculated = false;
/**
* Remember document numbers of ancestors during validation
*/
private int[] ancestorDocs = new int[2];
/**
* Reusable array that holds document numbers of parents.
*/
private int[] pDocs = new int[1];
/**
* Reusable array that holds a single document number.
*/
private final int[] singleDoc = new int[1];
/**
* The next document id to be returned
*/
private int currentDoc = -1;
/**
* Creates a new <code>DescendantSelfAxisScorer</code>.
*
* @param similarity the <code>Similarity</code> instance to use.
* @param reader for index access.
* @param hResolver the hierarchy resolver of <code>reader</code>.
*/
protected DescendantSelfAxisScorer(Similarity similarity,
IndexReader reader,
HierarchyResolver hResolver) {
super(similarity);
this.hResolver = hResolver;
// todo reuse BitSets?
this.contextHits = new BitSet(reader.maxDoc());
}
@Override
public int nextDoc() throws IOException {
if (currentDoc == NO_MORE_DOCS) {
return currentDoc;
}
collectContextHits();
if (contextHits.isEmpty()) {
currentDoc = NO_MORE_DOCS;
} else {
if (subScorer != null) {
currentDoc = subScorer.nextDoc();
} else {
currentDoc = NO_MORE_DOCS;
}
}
while (currentDoc != NO_MORE_DOCS) {
if (isValid(currentDoc)) {
return currentDoc;
}
// try next
currentDoc = subScorer.nextDoc();
}
return currentDoc;
}
@Override
public int docID() {
return currentDoc;
}
@Override
public float score() throws IOException {
return subScorer.score();
}
@Override
public int advance(int target) throws IOException {
if (currentDoc == NO_MORE_DOCS) {
return currentDoc;
}
// optimize in the case of an advance to finish.
// see https://issues.apache.org/jira/browse/JCR-3082
if (target == NO_MORE_DOCS) {
if (subScorer != null) {
subScorer.advance(target);
}
currentDoc = NO_MORE_DOCS;
return currentDoc;
}
currentDoc = subScorer.advance(target);
if (currentDoc == NO_MORE_DOCS) {
return NO_MORE_DOCS;
} else {
collectContextHits();
return isValid(currentDoc) ? currentDoc : nextDoc();
}
}
private void collectContextHits() throws IOException {
if (!contextHitsCalculated) {
long time = System.currentTimeMillis();
if (contextScorer != null) {
contextScorer.score(new AbstractHitCollector() {
@Override
protected void collect(int doc, float score) {
contextHits.set(doc);
}
}); // find all
}
contextHitsCalculated = true;
time = System.currentTimeMillis() - time;
if (log.isDebugEnabled()) {
log.debug("Collected {} context hits in {} ms for {}",
new Object[]{
contextHits.cardinality(),
time,
DescendantSelfAxisQuery.this
});
}
}
}
/**
* Returns <code>true</code> if <code>doc</code> is a valid match from
* the sub scorer against the context hits. The caller must ensure
* that the context hits are calculated before this method is called!
*
* @param doc the document number.
* @return <code>true</code> if <code>doc</code> is valid.
* @throws IOException if an error occurs while reading from the index.
*/
private boolean isValid(int doc) throws IOException {
// check self if necessary
if (minLevels == 0 && contextHits.get(doc)) {
return true;
}
// check if doc is a descendant of one of the context nodes
pDocs = hResolver.getParents(doc, pDocs);
if (pDocs.length == 0) {
return false;
}
int ancestorCount = 0;
// can only remember one parent doc per level
ancestorDocs[ancestorCount++] = pDocs[0];
// traverse
while (pDocs.length != 0) {
boolean valid = false;
for (int pDoc : pDocs) {
if (ancestorCount >= minLevels && contextHits.get(pDoc)) {
valid = true;
break;
}
}
if (valid) {
break;
} else {
// load next level
pDocs = getParents(pDocs, singleDoc);
// resize array if needed
if (ancestorCount == ancestorDocs.length) {
// double the size of the new array
int[] copy = new int[ancestorDocs.length * 2];
System.arraycopy(ancestorDocs, 0, copy, 0, ancestorDocs.length);
ancestorDocs = copy;
}
if (pDocs.length != 0) {
// can only remember one parent doc per level
ancestorDocs[ancestorCount++] = pDocs[0];
}
}
}
if (pDocs.length > 0) {
// since current parentDocs are descendants of one of the context
// docs we can promote all ancestorDocs to the context hits
for (int i = 0; i < ancestorCount; i++) {
contextHits.set(ancestorDocs[i]);
}
return true;
}
return false;
}
/**
* Returns the parent document numbers for the given <code>docs</code>.
*
* @param docs the current document numbers, for which to get the
* parents.
* @param pDocs an array of document numbers for reuse as return value.
* @return the parent document number for the given <code>docs</code>.
* @throws IOException if an error occurs while reading from the index.
*/
private int[] getParents(int[] docs, int[] pDocs) throws IOException {
// optimize single doc
if (docs.length == 1) {
return hResolver.getParents(docs[0], pDocs);
} else {
pDocs = new int[0];
for (int doc : docs) {
int[] p = hResolver.getParents(doc, new int[0]);
int[] tmp = new int[p.length + pDocs.length];
System.arraycopy(pDocs, 0, tmp, 0, pDocs.length);
System.arraycopy(p, 0, tmp, pDocs.length, p.length);
pDocs = tmp;
}
return pDocs;
}
}
}
}