/* See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Esri Inc. 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 com.esri.gpt.catalog.lucene;
import com.esri.gpt.catalog.discovery.Discoverable;
import com.esri.gpt.catalog.discovery.DiscoveryException;
import com.esri.gpt.catalog.discovery.LogicalClause;
import com.esri.gpt.catalog.discovery.PropertyClause;
import com.esri.gpt.catalog.discovery.PropertyComparisonType;
import com.esri.gpt.catalog.discovery.PropertyMeaning;
import com.esri.gpt.catalog.discovery.PropertyMeanings;
import com.esri.gpt.catalog.discovery.PropertyValueType;
import com.esri.gpt.catalog.discovery.SpatialClause;
import com.esri.gpt.framework.geometry.Envelope;
import com.esri.gpt.framework.util.DateProxy;
import com.esri.gpt.framework.util.Val;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.PrefixFilter;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
/**
* Provides query for specific field considering it's 'meaning'.
* Depending on the field name, it can create specific query just for that field.
*/
/**package*/class QueryProvider {
/** The Logger. */
private static Logger LOGGER = Logger.getLogger(QueryProvider.class.getName());
/** fields */
private String[] fields;
/** use constant score query */
private boolean useConstantScoreQuery;
/** lucene query adapter */
private LuceneQueryAdapter luceneQueryAdapter;
/** property meanings */
private PropertyMeanings meanings;
/**
* Creates instance of the provider.
* @param meanings meaning
*/
public QueryProvider(String[] fields, boolean useConstantScoreQuery, LuceneQueryAdapter luceneQueryAdapter, PropertyMeanings meanings) {
if (luceneQueryAdapter == null) {
throw new IllegalArgumentException("null luceneQueryAdapter.");
}
if (meanings == null) {
throw new IllegalArgumentException("null meanings.");
}
this.fields = fields;
this.useConstantScoreQuery = useConstantScoreQuery;
this.luceneQueryAdapter = luceneQueryAdapter;
this.meanings = meanings;
}
/**
* Gets a simple query.
* @param field field name
* @param queryText query text
* @param slop slop
* @return query or <code>null</code> if query for the particular field is unavailable
* @throws ParseException if error creating query
*/
protected Query getFieldQuery(String field, String queryText, int slop) throws ParseException {
Query q = null;
PropertyMeaning meaning = resolveMeaning(field);
if (meaning != null) {
PropertyComparisonType type = meaning.getComparisonType();
if (type == PropertyComparisonType.KEYWORD) {
q = new TermQuery(new Term(field, Val.chkStr(queryText).toLowerCase()));
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.STRING) {
q = new TermQuery(new Term(field, queryText));
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.LONG) {
try {
LongField lgField = new LongField(field);
queryText = lgField.makeValueToQuery(queryText, false, false);
q = new TermQuery(new Term(field, queryText));
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.DOUBLE) {
try {
DoubleField lgField = new DoubleField(field, DoubleField.DEFAULT_PRECISION);
queryText = lgField.makeValueToQuery(queryText, false, false);
q = new TermQuery(new Term(field, queryText));
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
q = new TermQuery(new Term(field, queryText));
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.TIMESTAMP) {
try {
if (isFullDate(queryText)) { // check if is this a full index date format?
TimestampField tsField = new TimestampField(field);
queryText = tsField.makeValueToQuery(queryText,true,false);
q = new TermQuery(new Term(field,queryText));
} else {
q = (new TimestampField(field)).makeRangeQuery(queryText,queryText,true,true);
}
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.GEOMETRY) {
try {
// create locator
Locator locator = Locator.newInstance();
// find best candidate
Locator.Candidate bestCandidate = locator.findBestCandidate(locator.find(queryText));
// create query if best candidate found
if (bestCandidate != null) {
double dif = 0.1;
// create query
BooleanQuery rootQuery = new BooleanQuery();
// create spatial
SpatialClause spatialClause = createSpatialClause(meaning, true);
// parse and set boounding box
spatialClause.getBoundingEnvelope().setMinX(bestCandidate.getLocation()[0] - dif);
spatialClause.getBoundingEnvelope().setMinY(bestCandidate.getLocation()[1] - dif);
spatialClause.getBoundingEnvelope().setMaxX(bestCandidate.getLocation()[0] + dif);
spatialClause.getBoundingEnvelope().setMaxY(bestCandidate.getLocation()[1] + dif);
// combine all together usingspatial clause adapter
SpatialClauseAdapter spatialClauseAdapter = new SpatialClauseAdapter(getLuceneQueryAdapter());
spatialClauseAdapter.adaptSpatialClause(rootQuery, new LogicalClause.LogicalAnd(), spatialClause);
// assign output
q = rootQuery;
}
} catch (Exception ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("QueryProvider.getFieldQuery(" + field + "," + queryText + "," + slop + ") -> " + q);
}
return q;
}
/**
* Gets prefix query.
* @param field field name
* @param termStr term
* @return query or <code>null</code> if query for the particular field is unavailable
* @throws ParseException if error creating query
*/
protected Query getPrefixQuery(String field, String termStr) throws ParseException {
Query q = null;
PropertyMeaning meaning = resolveMeaning(field);
if (meaning != null) {
PropertyComparisonType type = meaning.getComparisonType();
if (type == PropertyComparisonType.KEYWORD) {
q = newPrefixQuery(field, Val.chkStr(termStr).toLowerCase());
} else if (type == PropertyComparisonType.TERMS) {
q = newPrefixQuery(field, Val.chkStr(termStr).toLowerCase());
} else if (type == PropertyComparisonType.VALUE) {
q = newPrefixQuery(field, Val.chkStr(termStr));
} else {
q = newPrefixQuery(field, Val.chkStr(termStr));
}
} else if (field!=null) {
q = newPrefixQuery(field, Val.chkStr(termStr));
}
if (q==null) {
List clauses = new ArrayList();
for (int i = 0; i < getFields().length; i++) {
clauses.add(new BooleanClause(getPrefixQuery(getFields()[i], termStr),
BooleanClause.Occur.SHOULD));
}
q = newBooleanQuery(clauses, true);
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("QueryProvider.getPrefixQuery(" + field + "," + termStr + ") -> " + q);
}
return q;
}
/**
* Gets range query.
* @param field field name
* @param part1 first part of the range
* @param part2 second part of the range
* @param inclusive <code>true</code> for inclusive search
* @return query or <code>null</code> if query for the particular field is unavailable
* @throws ParseException if error creating query
*/
protected Query getRangeQuery(String field, String part1, String part2, boolean inclusive) throws ParseException {
Query q = null;
PropertyMeaning meaning = resolveMeaning(field);
if (meaning != null) {
PropertyComparisonType type = meaning.getComparisonType();
if (type == PropertyComparisonType.KEYWORD) {
q = newRangeQuery(field, Val.chkStr(part1).toLowerCase(), Val.chkStr(part2).toLowerCase(), inclusive);
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.STRING) {
q = newRangeQuery(field, part1, part2, inclusive);
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.GEOMETRY) {
try {
// create query
BooleanQuery rootQuery = new BooleanQuery();
// create spatial
SpatialClause spatialClause = createSpatialClause(meaning, inclusive);
// parse and set bounding box
parseEnvelope(spatialClause.getBoundingEnvelope(), part1, part2);
// combine all together using spatial clause adapter
SpatialClauseAdapter spatialClauseAdapter = new SpatialClauseAdapter(getLuceneQueryAdapter());
spatialClauseAdapter.adaptSpatialClause(rootQuery, new LogicalClause.LogicalAnd(), spatialClause);
// assign output
q = rootQuery;
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.TIMEPERIOD) {
try {
q = this.makeTimeperiodQuery(meaning,part1,part2,inclusive);
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.TIMESTAMP) {
try {
q = (new TimestampField(field)).makeRangeQuery(part1,part2,inclusive,inclusive);
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.LONG) {
String sLower = part1;
String sUpper = part2;
if (Val.chkStr(sLower).equals("*")) sLower= "";
if (Val.chkStr(sUpper).equals("*")) sUpper= "";
try {
q = (new LongField(field)).makeRangeQuery(sLower,sUpper,inclusive,inclusive);
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
if (type == PropertyComparisonType.VALUE && meaning.getValueType() == PropertyValueType.DOUBLE) {
String sLower = part1;
String sUpper = part2;
if (Val.chkStr(sLower).equals("*")) sLower= "";
if (Val.chkStr(sUpper).equals("*")) sUpper= "";
try {
q = (new DoubleField(field,DoubleField.DEFAULT_PRECISION)).makeRangeQuery(sLower,sUpper,inclusive,inclusive);
} catch (DiscoveryException ex) {
throw new ParseException("Error parsing expression: " + ex.getMessage());
}
}
} else if (field!=null) {
q = newRangeQuery(field, part1, part2, inclusive);
}
if (q==null) {
List clauses = new ArrayList();
for (int i = 0; i < getFields().length; i++) {
clauses.add(new BooleanClause(getRangeQuery(getFields()[i], part1, part2, inclusive),
BooleanClause.Occur.SHOULD));
}
q = newBooleanQuery(clauses, true);
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("QueryProvider.getRangeQuery(" + field + "," + part1 + "," + part2 + "," + inclusive + ") -> " + q);
}
return q;
}
/**
* Gets widcard query.
* @param field field name
* @param termStr term
* @return query or <code>null</code> if query for the particular field is unavailable
* @throws ParseException if error creating query
*/
protected Query getWildcardQuery(String field, String termStr) throws ParseException {
Query q = null;
PropertyMeaning meaning = resolveMeaning(field);
if (meaning != null) {
PropertyComparisonType type = meaning.getComparisonType();
if (type == PropertyComparisonType.KEYWORD) {
q = new WildcardQuery(new Term(field, Val.chkStr(termStr).toLowerCase()));
}
if (type == PropertyComparisonType.VALUE) {
q = new WildcardQuery(new Term(field, termStr));
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("QueryProvider.getWildcardQuery(" + field + "," + termStr + ") -> " + q);
}
return q;
}
/**
* Gets fuzzy query.
* @param field field name
* @param termStr term
* @param minSimilarity minimal similarity
* @return query or <code>null</code> if query for the particular field is unavailable
* @throws ParseException if error creating query
*/
protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException {
Query q = null;
PropertyMeaning meaning = resolveMeaning(field);
if (meaning != null) {
PropertyComparisonType type = meaning.getComparisonType();
if (type == PropertyComparisonType.KEYWORD) {
q = new FuzzyQuery(new Term(field, Val.chkStr(termStr).toLowerCase()));
}
if (type == PropertyComparisonType.VALUE) {
q = new FuzzyQuery(new Term(field, termStr));
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("QueryProvider.getFuzzyQuery(" + field + "," + termStr + "," + minSimilarity + ") -> " + q);
}
return q;
}
/**
* Creates new prefix query. Depending on {@link getUseConstantScoreQuery()}
* it's either {@link org.apache.lucene.search.ConstantScoreQuery} with
* {@link org.apache.lucene.search.RangeFilter} or just
* {@link org.apache.lucene.search.RangeQuery}.
* @param term term
* @return prefix query
*/
private Query newRangeQuery(String fieldName, String lowerTerm, String upperTerm, boolean inclusive) {
// TODO this was changed
// return this.getUseConstantScoreQuery()?
// new ConstantScoreQuery(new RangeFilter(fieldName, lowerTerm, upperTerm, inclusive, inclusive)):
// new RangeQuery(new Term(fieldName, lowerTerm), new Term(fieldName, upperTerm), inclusive);
return new TermRangeQuery(fieldName,lowerTerm,upperTerm,inclusive,inclusive);
}
/**
* Creates new prefix query. Depending on {@link getUseConstantScoreQuery()}
* it's either {@link org.apache.lucene.search.ConstantScoreQuery} with
* {@link org.apache.lucene.search.PrefixFilter} or just
* {@link org.apache.lucene.search.PrefixQuery}.
* @param term term
* @return prefix query
*/
private Query newPrefixQuery(String fieldName, String term) {
return this.getUseConstantScoreQuery()?
new ConstantScoreQuery(new PrefixFilter(new Term(fieldName,term))):
new PrefixQuery(new Term(fieldName,term));
}
/**
* Gets boolean query. Identical to the method {@link org.apache.lucene.queryParser.MultiFieldQueryParser#getBooleanQuery}
* @param clauses list of clauses
* @param disableCoord disables {@link org.apache.lucene.search.Similarity#coord(int,int)} in scoring
* @return
* @throws ParseException
*/
private Query newBooleanQuery(List clauses, boolean disableCoord)
throws ParseException {
if (clauses.size() == 0) {
return null; // all clause words were filtered away by the analyzer.
}
BooleanQuery query = new BooleanQuery(disableCoord);
for (int i = 0; i < clauses.size(); i++) {
query.add((BooleanClause) clauses.get(i));
}
return query;
}
/**
* Checks if provided date is a full date stored in the index. Full date is
* a date of milliseconds resolution.
* @param queryText possibly a full date
* @return <code>true</code> if this is a full date.
*/
private boolean isFullDate(String queryText) {
try {
queryText = Val.chkStr(queryText);
long lngDate = DateTools.stringToTime(queryText);
return queryText.matches("[0-9]+") && queryText.length() >= DateTools.timeToString(lngDate, DateTools.Resolution.MILLISECOND).length();
} catch (java.text.ParseException ex) {
return false;
}
}
/**
* Makes the value to query.
* <br/>The value to query is derived from timestampToIndexableString().
* @param value to input query value
* @param isLowerBoundary true if this is a lower boundary of a range query
* @param isUpperBoundary true if this is a upper boundary of a range query
* @param inclusive <code>true</code> to make inclusive query
* @return the value to query
* @throws DiscoveryException if the supplied value cannot be converted
*/
private String makeValueToQuery(String value, boolean isLowerBoundary, boolean isUpperBoundary, boolean inclusive) {
DateProxy proxy = new DateProxy();
proxy.setDate(value);
if (!proxy.getIsValid()) {
throw new IllegalArgumentException("Invalid Timestamp: " + value
+ ", use for yyyy-mm-dd hh:mm:ss.fff");
}
Timestamp tsValue = null;
if (isLowerBoundary) {
tsValue = inclusive ? proxy.asFromTimestamp() : proxy.asFromTimestampExcl();
} else if (isUpperBoundary) {
tsValue = inclusive ? proxy.asToTimestamp() : proxy.asToTimestampExcl();
} else {
tsValue = inclusive ? proxy.asFromTimestamp() : proxy.asFromTimestampExcl();
}
if (tsValue == null) {
return null;
}
if (isLowerBoundary) {
LOGGER.finer("Lower boundary timestamp to query: " + tsValue);
} else if (isUpperBoundary) {
LOGGER.finer("Upper boundary timestamp to query: " + tsValue);
} else {
LOGGER.finer("Timestamp to query: " + tsValue);
}
return TimestampField.timestampToIndexableString(tsValue);
}
/**
* Parses corner.
* @param cornerDef corner definition
* @param extremeValue array of two extreme values for that corner
* @return array of coordinates of the corner
* @throws ParseException if parsing fails
*/
private double[] parseCorner(String cornerDef, double[] extremeValue) throws ParseException {
double[] corner = new double[]{extremeValue[0], extremeValue[1]};
String[] sCoords = Val.chkStr(cornerDef).split(",");
if (sCoords.length == 2) {
for (int i = 0; i < 2; i++) {
if (sCoords[i].trim().equals("*")) {
// that's o.k; already set to extreme value
} else {
try {
corner[i] = Double.parseDouble(sCoords[i].trim());
} catch (NumberFormatException ex) {
throw new ParseException("Invalid envelope corner definition: " + cornerDef);
}
}
}
} else if (sCoords.length == 1) {
if (sCoords[0].trim().equals("*")) {
// that's o.k; already set to extreme value; in fact if sCoords.length==1
// the only allowed value is (*)
} else {
throw new ParseException("Invalid envelope corner definition: " + cornerDef);
}
} else {
throw new ParseException("Invalid envelope corner definition: " + cornerDef);
}
return corner;
}
/**
* Parses envelope.
* @param envelope envelope to store information
* @param part1 left part of the range query
* @param part2 right part of the range query
* @throws ParseException if parsing envelope fails
*/
private void parseEnvelope(Envelope envelope, String part1, String part2) throws ParseException {
double[] loverLeftCorner = parseCorner(part1, new double[]{-180, -90});
double[] upperRightCorner = parseCorner(part2, new double[]{+180, +90});
envelope.setMinX(loverLeftCorner[0]);
envelope.setMinY(loverLeftCorner[1]);
envelope.setMaxX(upperRightCorner[0]);
envelope.setMaxY(upperRightCorner[1]);
}
/**
* Creates spatial clause.
* @param meaning property meaning
* @param inclusive <code>true</code> to have inclusive clause
* @return clause
*/
private SpatialClause createSpatialClause(PropertyMeaning meaning, boolean inclusive) {
SpatialClause spatialClause = inclusive
? new SpatialClause.GeometryBBOXIntersects()
: new SpatialClause.GeometryIsWithin();
spatialClause.setSrsName("4326");
Discoverable target = new Discoverable(meaning.getName());
target.setMeaning(meaning);
target.setStoreable(new GeometryProperty(meaning.getName()));
spatialClause.setTarget(target);
return spatialClause;
}
/**
* Makes a timeperiod query.
* @param meaning the property meaning
* @param the lower bound for the range
* @param the upper bound for the range
* @param inclusive true if the range is inclusive
* @return the query
* @throws ParseException
* @throws DiscoveryException
*/
private Query makeTimeperiodQuery(PropertyMeaning meaning,
String lower, String upper, boolean inclusive)
throws DiscoveryException, ParseException {
// make the clause
Discoverable target = new Discoverable(meaning.getName());
target.setMeaning(meaning);
target.setStoreable(new TimeperiodProperty(meaning.getName()));
PropertyClause.PropertyIsBetween clause = new PropertyClause.PropertyIsBetween();
clause.setTarget(target);
clause.setLowerBoundary(lower);
clause.setUpperBoundary(upper);
// make the query
BooleanQuery query = new BooleanQuery();
TimeperiodClauseAdapter adaptor = new TimeperiodClauseAdapter(getLuceneQueryAdapter());
adaptor.setInclusive(inclusive);
adaptor.adaptPropertyClause(query,new LogicalClause.LogicalAnd(),clause);
return query;
}
/**
* Resolves meaning.
* @param fieldName field name
* @return meaning proxy
*/
private PropertyMeaning resolveMeaning(String fieldName) {
PropertyMeaning meaning = getMeanings().get(fieldName);
if (meaning == null) {
Discoverable discoverable = getMeanings().getAllAliased().get(fieldName);
if (discoverable != null) {
meaning = discoverable.getMeaning();
}
}
return meaning;
}
/**
* @return the fields
*/
protected String[] getFields() {
return fields;
}
/**
* @return the useConstantScoreQuery
*/
protected boolean getUseConstantScoreQuery() {
return useConstantScoreQuery;
}
/**
* @return the luceneQueryAdapter
*/
protected LuceneQueryAdapter getLuceneQueryAdapter() {
return luceneQueryAdapter;
}
/**
* @return the meanings
*/
protected PropertyMeanings getMeanings() {
return meanings;
}
}