/*
* 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.exoplatform.services.jcr.impl.core.query.lucene;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.search.BooleanQuery;
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.TermRangeQuery;
import org.apache.lucene.search.Weight;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Implements a lucene range query.
*/
public class RangeQuery extends Query implements Transformable
{
/**
* The serial version UID
*/
private static final long serialVersionUID = 6232103337968115020L;
/**
* The lower term. May be <code>null</code> if <code>upperTerm</code> is not
* <code>null</code>.
*/
private Term lowerTerm;
/**
* The upper term. May be <code>null</code> if <code>lowerTerm</code> is not
* <code>null</code>.
*/
private Term upperTerm;
/**
* If <code>true</code> the range interval is inclusive.
*/
private boolean inclusive;
/**
* How the term enum is transformed before it is compared to lower and upper
* term.
*/
private int transform = TRANSFORM_NONE;
/**
* The rewritten range query or <code>null</code> if the range spans more
* than {@link org.apache.lucene.search.BooleanQuery#maxClauseCount} terms.
*/
private Query stdRangeQuery;
/**
* Creates a new RangeQuery. The lower or the upper term may be
* <code>null</code>, but not both!
*
* @param lowerTerm the lower term of the interval, or <code>null</code>
* @param upperTerm the upper term of the interval, or <code>null</code>.
* @param inclusive if <code>true</code> the interval is inclusive.
*/
public RangeQuery(Term lowerTerm, Term upperTerm, boolean inclusive)
{
this(lowerTerm, upperTerm, inclusive, TRANSFORM_NONE);
}
/**
* Creates a new RangeQuery. The lower or the upper term may be
* <code>null</code>, but not both!
*
* @param lowerTerm the lower term of the interval, or <code>null</code>
* @param upperTerm the upper term of the interval, or <code>null</code>.
* @param inclusive if <code>true</code> the interval is inclusive.
* @param transform how term enums are transformed when read from the index.
*/
public RangeQuery(Term lowerTerm, Term upperTerm, boolean inclusive, int transform)
{
if (lowerTerm == null && upperTerm == null)
{
throw new IllegalArgumentException("At least one term must be non-null");
}
if (lowerTerm != null && upperTerm != null && lowerTerm.field() != upperTerm.field())
{
throw new IllegalArgumentException("Both terms must be for the same field");
}
// if we have a lowerTerm, start there. otherwise, start at beginning
if (lowerTerm != null)
{
this.lowerTerm = lowerTerm;
}
else
{
this.lowerTerm = new Term(upperTerm.field(), "");
}
this.upperTerm = upperTerm;
this.inclusive = inclusive;
this.transform = transform;
}
/**
* {@inheritDoc}
*/
public void setTransformation(int transformation)
{
this.transform = transformation;
}
/**
* Tries to rewrite this query into a standard lucene RangeQuery.
* This rewrite might fail with a TooManyClauses exception. If that
* happens, we use our own implementation.
*
* @param reader the index reader.
* @return the rewritten query or this query if rewriting is not possible.
* @throws IOException if an error occurs.
*/
public Query rewrite(IndexReader reader) throws IOException
{
if (transform == TRANSFORM_NONE)
{
Query stdRangeQueryImpl =
new TermRangeQuery(lowerTerm.field(), lowerTerm.text(), upperTerm.text(), inclusive, inclusive);
try
{
stdRangeQuery = stdRangeQueryImpl.rewrite(reader);
return stdRangeQuery;
}
catch (BooleanQuery.TooManyClauses e)
{
// failed, use own implementation
return this;
}
}
else
{
// always use our implementation when we need to transform the
// term enum
return this;
}
}
/**
* Creates the <code>Weight</code> for this query.
*
* @param searcher the searcher to use for the <code>Weight</code>.
* @return the <code>Weigth</code> for this query.
*/
public Weight createWeight(Searcher searcher)
{
return new RangeQueryWeight(searcher);
}
/**
* Returns a string representation of this query.
* @param field the field name for which to create a string representation.
* @return a string representation of this query.
*/
public String toString(String field)
{
StringBuilder buffer = new StringBuilder();
if (!getField().equals(field))
{
buffer.append(getField());
buffer.append(":");
}
buffer.append(inclusive ? "[" : "{");
buffer.append(lowerTerm != null ? lowerTerm.text() : "null");
buffer.append(" TO ");
buffer.append(upperTerm != null ? upperTerm.text() : "null");
buffer.append(inclusive ? "]" : "}");
if (getBoost() != 1.0f)
{
buffer.append("^");
buffer.append(Float.toString(getBoost()));
}
return buffer.toString();
}
/**
* {@inheritDoc}
*/
public void extractTerms(Set<Term> terms)
{
if (stdRangeQuery != null)
{
stdRangeQuery.extractTerms(terms);
}
}
/**
* Returns the field name for this query.
*/
private String getField()
{
return (lowerTerm != null ? lowerTerm.field() : upperTerm.field());
}
//--------------------------< RangeQueryWeight >----------------------------
/**
* The <code>Weight</code> implementation for this <code>RangeQuery</code>.
*/
private class RangeQueryWeight extends AbstractWeight
{
private static final long serialVersionUID = -3768950544626254226L;
/**
* Creates a new <code>RangeQueryWeight</code> instance using
* <code>searcher</code>.
*
* @param searcher a <code>Searcher</code> instance.
*/
RangeQueryWeight(Searcher searcher)
{
super(searcher);
}
/**
* Creates a {@link RangeQueryScorer} instance.
*
* @param reader index reader
* @return a {@link RangeQueryScorer} instance
*/
@Override
protected Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer)
{
return new RangeQueryScorer(searcher.getSimilarity(), reader);
};
/**
* Returns this <code>RangeQuery</code>.
*
* @return this <code>RangeQuery</code>.
*/
@Override
public Query getQuery()
{
return RangeQuery.this;
}
/**
* {@inheritDoc}
*/
@Override
public float getValue()
{
return 1.0f;
}
/**
* {@inheritDoc}
*/
@Override
public float sumOfSquaredWeights() throws IOException
{
return 1.0f;
}
/**
* {@inheritDoc}
*/
@Override
public void normalize(float norm)
{
}
/**
* {@inheritDoc}
*/
@Override
public Explanation explain(IndexReader reader, int doc) throws IOException
{
return new Explanation();
}
}
//------------------------< RangeQueryScorer >------------------------------
/**
* Implements a <code>Scorer</code> for this <code>RangeQuery</code>.
*/
private final class RangeQueryScorer extends Scorer
{
/**
* The index reader to use for calculating the matching documents.
*/
private final IndexReader reader;
/**
* The documents ids that match this range query.
*/
private final BitSet hits;
/**
* Set to <code>true</code> when the hits have been calculated.
*/
private boolean hitsCalculated = false;
/**
* The next document id to return
*/
private int nextDoc = -1;
/**
* The cache key to use to store the results.
*/
private final String cacheKey;
/**
* The map to store the results.
*/
private final Map<String, BitSet> resultMap;
/**
* Creates a new RangeQueryScorer.
* @param similarity the similarity implementation.
* @param reader the index reader to use.
*/
@SuppressWarnings({"unchecked"})
RangeQueryScorer(Similarity similarity, IndexReader reader)
{
super(similarity);
this.reader = reader;
StringBuilder key = new StringBuilder();
key.append(lowerTerm != null ? lowerTerm.field() : upperTerm.field());
key.append('\uFFFF');
key.append(lowerTerm != null ? lowerTerm.text() : "");
key.append('\uFFFF');
key.append(upperTerm != null ? upperTerm.text() : "");
key.append('\uFFFF');
key.append(inclusive);
key.append('\uFFFF');
key.append(transform);
this.cacheKey = key.toString();
// check cache
PerQueryCache cache = PerQueryCache.getInstance();
Map<String, BitSet> m = (Map<String, BitSet>)cache.get(RangeQueryScorer.class, reader);
if (m == null)
{
m = new HashMap<String, BitSet>();
cache.put(RangeQueryScorer.class, reader, m);
}
resultMap = m;
BitSet result = resultMap.get(cacheKey);
if (result == null)
{
result = new BitSet(reader.maxDoc());
}
else
{
hitsCalculated = true;
}
hits = result;
}
@Override
public int nextDoc() throws IOException
{
if (nextDoc == NO_MORE_DOCS)
{
return nextDoc;
}
calculateHits();
nextDoc = hits.nextSetBit(nextDoc + 1);
if (nextDoc < 0)
{
nextDoc = NO_MORE_DOCS;
}
return nextDoc;
}
@Override
public int docID()
{
return nextDoc;
}
@Override
public float score()
{
return 1.0f;
}
@Override
public int advance(int target) throws IOException
{
if (nextDoc == NO_MORE_DOCS)
{
return nextDoc;
}
calculateHits();
nextDoc = hits.nextSetBit(target);
if (nextDoc < 0)
{
nextDoc = NO_MORE_DOCS;
}
return nextDoc;
}
/**
* Calculates the ids of the documents matching this range query.
* @throws IOException if an error occurs while reading from the index.
*/
private void calculateHits() throws IOException
{
if (hitsCalculated)
{
return;
}
String testField = getField();
boolean checkLower = false;
if (!inclusive || transform != TRANSFORM_NONE)
{
// make adjustments to set to exclusive
checkLower = true;
}
int propNameLength = FieldNames.getNameLength(lowerTerm.text());
String namePrefix = "";
if (propNameLength > 0)
{
namePrefix = lowerTerm.text().substring(0, propNameLength);
}
List<Term> startTerms = new ArrayList<Term>(2);
if (transform == TRANSFORM_NONE || lowerTerm.text().length() <= propNameLength)
{
// use lowerTerm as is
startTerms.add(lowerTerm);
}
else
{
// first enumerate terms using lower case start character
StringBuilder termText = new StringBuilder(propNameLength + 1);
termText.append(lowerTerm.text().subSequence(0, propNameLength));
char startCharacter = lowerTerm.text().charAt(propNameLength);
termText.append(Character.toLowerCase(startCharacter));
startTerms.add(new Term(lowerTerm.field(), termText.toString()));
// second enumerate terms using upper case start character
termText.setCharAt(termText.length() - 1, Character.toUpperCase(startCharacter));
startTerms.add(new Term(lowerTerm.field(), termText.toString()));
}
for (Term startTerm : startTerms)
{
TermEnum terms = reader.terms(startTerm);
try
{
TermDocs docs = reader.termDocs();
try
{
do
{
Term term = terms.term();
if (term != null && term.field() == testField && term.text().startsWith(namePrefix))
{
if (checkLower)
{
int compare = termCompare(term.text(), lowerTerm.text(), propNameLength);
if (compare > 0 || compare == 0 && inclusive)
{
// do not check lower term anymore if no
// transformation is done on the term enum
checkLower = transform != TRANSFORM_NONE;
}
else
{
// continue with next term
continue;
}
}
if (upperTerm != null)
{
int compare = termCompare(term.text(), upperTerm.text(), propNameLength);
// if beyond the upper term, or is exclusive and
// this is equal to the upper term
if ((compare > 0) || (!inclusive && compare == 0))
{
// only break out if no transformation
// was done on the term from the enum
if (transform == TRANSFORM_NONE)
{
break;
}
else
{
// because of the transformation
// it is possible that the next
// term will be included again if
// we still enumerate on the same
// property name
if (term.text().startsWith(namePrefix))
{
continue;
}
else
{
break;
}
}
}
}
docs.seek(terms);
while (docs.next())
{
hits.set(docs.doc());
}
}
else
{
break;
}
}
while (terms.next());
}
finally
{
docs.close();
}
}
finally
{
terms.close();
}
}
hitsCalculated = true;
// put to cache
resultMap.put(cacheKey, hits);
}
/**
* Compares the <code>text</code> with the <code>other</code> String. This
* implementation behaves like {@link String#compareTo(Object)} but also
* respects the {@link RangeQuery#transform} property.
*
* @param text the text to compare to <code>other</code>. The
* transformation function is applied to this parameter before
* it is compared to <code>other</code>.
* @param other the other String.
* @param offset start comparing the two strings at <code>offset</code>.
* @return see {@link String#compareTo(Object)}. But also respects {@link
* RangeQuery#transform}.
*/
private int termCompare(String text, String other, int offset)
{
OffsetCharSequence seq1 = new OffsetCharSequence(offset, text, transform);
OffsetCharSequence seq2 = new OffsetCharSequence(offset, other);
return seq1.compareTo(seq2);
}
}
}