/* 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.PropertyClause.PropertyIsBetween; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsEqualTo; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsGreaterThan; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsGreaterThanOrEqualTo; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsLessThan; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsLessThanOrEqualTo; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsNotEqualTo; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsNull; import com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsLike; import com.esri.gpt.catalog.schema.indexable.tp.TpUtil; import com.esri.gpt.framework.util.Val; import java.io.IOException; import java.util.Calendar; import java.util.Collection; import java.util.GregorianCalendar; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.index.IndexReader.FieldOption; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; /** * Adapts a timeperiod based PropertyClause to the Lucene model. */ public class TimeperiodClauseAdapter extends PropertyClauseAdapter { /** class variables ========================================================= */ /** The Logger. */ private static Logger LOGGER = Logger.getLogger(TimeperiodClauseAdapter.class.getName()); /** instance variables ====================================================== */ private String baseFieldName; private boolean inclusive = true; private int maxIntervalFieldName; private String intervalMetaFieldName; private String multiplicityFieldName; private Long now = System.currentTimeMillis(); private int precisionStep = 4; private String summaryMetaFieldName; private boolean queryIntersectsNow; private boolean queryIsAfterNow; private boolean queryIsBeforeNow; private Long queryLower; private Long queryUpper; /** constructors ============================================================ */ /** * Constructs with an associated query adapter. * @param queryAdapter the query adapter */ protected TimeperiodClauseAdapter(LuceneQueryAdapter queryAdapter) { super(queryAdapter); } /** properties ============================================================== */ /** * Determines if range boundaries inclusive or exclusive. * <br/>[] inclusive -> intersects * <br/>{} exclusive -> within * @return true if inclusive */ public boolean getInclusive() { return this.inclusive; } /** * Determines if range boundaries inclusive or exclusive. * <br/>[] inclusive -> intersects * <br/>{} exclusive -> within * @param inclusive true if inclusive */ public void setInclusive(boolean inclusive) { this.inclusive = inclusive; } /** methods ================================================================= */ /** * Adapts a timeperiod based PropertyClause to the Lucene model. * @param activeBooleanQuery the active Lucene boolean query * @param activeLogicalClause the active discovery logical clause * @param propertyClause the property clause to adapt * @throws DiscoveryException if an invalid clause is encountered * @throws ParseException if a Lucene query parsing exception occurs */ protected void adaptPropertyClause(BooleanQuery activeBooleanQuery, LogicalClause activeLogicalClause, PropertyClause propertyClause) throws DiscoveryException, ParseException { LOGGER.finer("Adapting timeperiod PropertyClause...\n"+propertyClause); // determine the discoverable target, set the underlying storable Discoverable discoverable = propertyClause.getTarget(); if (discoverable == null) { String sErr = "The PropertyClause.target is null."; throw new DiscoveryException(sErr); } if (discoverable.getStorable() == null) { String sErr = "The PropertyClause.target.storeable is null."; throw new DiscoveryException(sErr); } else { Storeable storeable = (Storeable)discoverable.getStorable(); this.baseFieldName = storeable.getName(); if (this.baseFieldName.endsWith(".intersects")) { this.inclusive = true; this.baseFieldName = this.baseFieldName.substring(0,this.baseFieldName.length()-11); } else if (this.baseFieldName.endsWith(".within")) { this.inclusive = false; this.baseFieldName = this.baseFieldName.substring(0,this.baseFieldName.length()-7); } this.intervalMetaFieldName = this.baseFieldName+".imeta"; this.multiplicityFieldName = this.baseFieldName+".num"; this.summaryMetaFieldName = this.baseFieldName+".meta"; } // initialize values boolean bInclusive = this.inclusive; String sLiteral = Val.chkStr(propertyClause.getLiteral()); String sLower = ""; String sUpper = ""; String sErr = null; String sErrSfx = " is not supported for timeperiod fields,"+ " use PropertyIsBetween."; if (propertyClause instanceof PropertyIsBetween) { PropertyIsBetween between = (PropertyIsBetween)propertyClause; sLower = Val.chkStr(between.getLowerBoundary()); sUpper = Val.chkStr(between.getUpperBoundary()); this.queryLower = this.parseDateTime(sLower,false); this.queryUpper = this.parseDateTime(sUpper,true); } else if ((propertyClause instanceof PropertyIsEqualTo) || (propertyClause instanceof PropertyIsNotEqualTo)) { Query q = null; sLower = Val.chkStr(sLiteral); sUpper = Val.chkStr(sLiteral); this.queryLower = this.parseDateTime(sLower,false); if (this.queryLower == null) { sErr = "Timeperiod literal cannot be null for PropertyIsEqualTo/PropertyIsNotEqualTo"; } else { this.queryUpper = this.parseDateTime(sUpper,true); if (propertyClause instanceof PropertyIsEqualTo) { q = this.makeEquals(); } else { q = this.makeNotEquals(); } appendQuery(activeBooleanQuery,activeLogicalClause,q); return; } } else if (propertyClause instanceof PropertyIsGreaterThan) { bInclusive = false; // use within logic sLower = sLiteral; this.queryLower = this.parseDateTime(sLower,false); if (this.queryLower != null) { this.queryLower = new Long(this.queryLower.longValue() + 1); } } else if (propertyClause instanceof PropertyIsGreaterThanOrEqualTo) { bInclusive = false; // use within logic sLower = sLiteral; this.queryLower = this.parseDateTime(sLower,false); } else if (propertyClause instanceof PropertyIsLessThan) { bInclusive = false; // use within logic sUpper = sLiteral; this.queryUpper = this.parseDateTime(sUpper,false); if (this.queryUpper != null) { this.queryUpper = new Long(this.queryUpper.longValue() - 1); } } else if (propertyClause instanceof PropertyIsLessThanOrEqualTo) { bInclusive = false; // use within logic sUpper = sLiteral; this.queryUpper = this.parseDateTime(sUpper,true); } else if (propertyClause instanceof PropertyIsNull) { appendQuery(activeBooleanQuery,activeLogicalClause,this.makeNull()); return; } else if (propertyClause instanceof PropertyIsLike) { sErr = "PropertyIsLike"+sErrSfx; } else { sErr = "Unrecognized property clause type: "+propertyClause.getClass().getName(); } if (sErr != null) { throw new DiscoveryException(sErr); } // check for upper < lower if ((this.queryLower != null) && (this.queryUpper != null)) { if (this.queryUpper.longValue() < this.queryLower.longValue()) { appendQuery(activeBooleanQuery,activeLogicalClause,new BooleanQuery()); return; } } // could implement a timeperiod relevance ranking here if ((this.queryLower != null) && (this.queryUpper != null)) {} if (bInclusive) { this.determineRelationshipWithNow(); this.determineMaxIntervalFieldName(); appendQuery(activeBooleanQuery,activeLogicalClause,makeIntersects()); } else { this.determineRelationshipWithNow(); appendQuery(activeBooleanQuery,activeLogicalClause,makeWithin()); } } /** * Determine the index for the highest interval field within the Lucene index. * <br/>e.g. timeperiod.l.7 * <br/>If the the document with the most intervals has 7, then 7 is the max. * @throws DiscoveryException if there is a problem accessing the index */ private void determineMaxIntervalFieldName() throws DiscoveryException { IndexSearcher searcher = null; try { searcher = this.getQueryAdapter().getIndexAdapter().newSearcher(); IndexReader reader = searcher.getIndexReader(); Collection<String> names = reader.getFieldNames(FieldOption.ALL); String sPfx = this.baseFieldName.toLowerCase()+".l."; int nBeginSubstring = sPfx.length(); int nMax = -1; for (String name: names) { String lc = name.toLowerCase(); if (lc.startsWith(sPfx)) { LOGGER.finest("Found boundary field: "+name); String s = lc.substring(nBeginSubstring); try { int n = Integer.valueOf(s); if (n > nMax) { nMax = n; } } catch (NumberFormatException nfe) {} } } LOGGER.finest("MaxBndFieldIndex: "+nMax); this.maxIntervalFieldName = nMax; } catch (IOException e) { LOGGER.log(Level.SEVERE,"Index issue.",e); throw new DiscoveryException(e.toString(),e); } finally { this.getQueryAdapter().getIndexAdapter().closeSearcher(searcher); } } /** * Determines if the query bounds are before after or intersecting with now. */ private void determineRelationshipWithNow() { long nNow = this.now; if ((this.queryLower != null) && (this.queryLower.longValue() > nNow)) { this.queryIsAfterNow = true; } else if ((this.queryUpper != null) && (this.queryUpper.longValue() < nNow)) { this.queryIsBeforeNow = true; } else { this.queryIntersectsNow = true; } } /** * Makes the lower boundary field name associated with an interval index. * <br/>Interval 0 is the summary interval for the document. * @param interval the interval index * @return the name */ private String getLowerFieldName(int interval) { //if (interval == 0) return this.baseFieldName+".l.d"; //else return this.baseFieldName+".l."+interval; return this.baseFieldName+".l."+interval; } /** * Makes the meta value associated with an interval index. * <br/>Interval 0 is the summary interval for the document. * @param type the value predicate * @param interval the interval index * @return the name */ private String getMetaValue(String type, int interval) { //if (interval == 0) return type+".d"; //else return type+"."+interval; return type+"."+interval; } /** * Makes the upper boundary field name associated with an interval index. * <br/>Interval 0 is the summary interval for the document. * @param interval the interval index * @return the name */ private String getUpperFieldName(int interval) { //if (interval == 0) return this.baseFieldName+".u.d"; //else return this.baseFieldName+".u."+interval; return this.baseFieldName+".u."+interval; } /** * Constructs a query for documents that are equal to the * input time period. * @return the query */ private Query makeEquals() { /** * one determinate and boundaries are equal */ int nStep = this.precisionStep; String fSMeta = this.summaryMetaFieldName; String fLower = this.getLowerFieldName(0); String fUpper = this.getUpperFieldName(0); String sMeta = "is1determinate"; Query qIs1Determinate = new TermQuery(new Term(fSMeta,sMeta)); Query qDocLowerEq = NumericRangeQuery.newLongRange( fLower,nStep,queryLower,queryLower,true,true); Query qDocUpperEq = NumericRangeQuery.newLongRange( fUpper,nStep,queryUpper,queryUpper,true,true); BooleanQuery bq = new BooleanQuery(); bq.add(qIs1Determinate,BooleanClause.Occur.MUST); bq.add(qDocLowerEq,BooleanClause.Occur.MUST); bq.add(qDocUpperEq,BooleanClause.Occur.MUST); return bq; } /** * Constructs a query for documents that intersect the input time period. * @return the query */ private Query makeIntersects() { BooleanQuery bq = new BooleanQuery(); for (int i=1;i<=this.maxIntervalFieldName;i++) { Query q = this.makeIntersectsInterval(i); bq.add(q,BooleanClause.Occur.SHOULD); } return bq; } /** * Constructs a query for a document interval that intersects * the input time period. * @param interval the field name index for the interval * @return the query */ private Query makeIntersectsInterval(int interval) { /* Intersects: docMinIn: fMin >= qMin AND fMin <= qMax OR docMaxIn: fMax >= qMin AND fMax <= qMax OR docContains: fMin <= qMin AND fMax >= qMax */ int nStep = this.precisionStep; String fMeta = this.intervalMetaFieldName; String fLower = this.getLowerFieldName(interval); String fUpper = this.getUpperFieldName(interval); Query qDocLowerIn = NumericRangeQuery.newLongRange( fLower,nStep,queryLower,queryUpper,true,true); Query qDocUpperIn = NumericRangeQuery.newLongRange( fUpper,nStep,queryLower,queryUpper,true,true); BooleanQuery qDocContains = new BooleanQuery(); Query qLowerBeforeL = NumericRangeQuery.newLongRange( fLower,nStep,null,queryLower,true,true); Query qLowerBeforeU = NumericRangeQuery.newLongRange( fLower,nStep,null,queryUpper,true,true); Query qUpperAfterL = NumericRangeQuery.newLongRange( fUpper,nStep,queryLower,null,true,true); Query qUpperAfterU = NumericRangeQuery.newLongRange( fUpper,nStep,queryUpper,null,true,true); qDocContains.add(qLowerBeforeL,BooleanClause.Occur.MUST); qDocContains.add(qLowerBeforeU,BooleanClause.Occur.MUST); qDocContains.add(qUpperAfterL,BooleanClause.Occur.MUST); qDocContains.add(qUpperAfterU,BooleanClause.Occur.MUST); BooleanQuery qIntervalIn = new BooleanQuery(); qIntervalIn.add(qDocLowerIn,BooleanClause.Occur.SHOULD); qIntervalIn.add(qDocUpperIn,BooleanClause.Occur.SHOULD); qIntervalIn.add(qDocContains,BooleanClause.Occur.SHOULD); String sMeta = this.getMetaValue("determinate",interval); Query qIsDeterminate = new TermQuery(new Term(fMeta,sMeta)); BooleanQuery bqDeterminate = new BooleanQuery(); bqDeterminate.add(qIsDeterminate,BooleanClause.Occur.MUST); bqDeterminate.add(qIntervalIn,BooleanClause.Occur.MUST); // intervals that intersect now BooleanQuery bqNow = new BooleanQuery(); if (this.queryIntersectsNow) { // any interval with the following meta terms intersects: // now.i now.l.i now.u.i where i is the interval index (1 based) String s1 = this.getMetaValue("now",interval); String s2 = this.getMetaValue("now.l",interval); String s3 = this.getMetaValue("now.u",interval); Query q1 = new TermQuery(new Term(fMeta,s1)); Query q2 = new TermQuery(new Term(fMeta,s2)); Query q3 = new TermQuery(new Term(fMeta,s3)); bqNow.add(q1,BooleanClause.Occur.SHOULD); bqNow.add(q2,BooleanClause.Occur.SHOULD); bqNow.add(q3,BooleanClause.Occur.SHOULD); } else if (this.queryIsBeforeNow) { // meta term now.u.i and fLower must be <= queryUpper String s1 = this.getMetaValue("now.u",interval); Query q1 = new TermQuery(new Term(fMeta,s1)); Query q2 = NumericRangeQuery.newLongRange( fLower,nStep,null,queryUpper,true,true); bqNow.add(q1,BooleanClause.Occur.MUST); bqNow.add(q2,BooleanClause.Occur.MUST); } else if (this.queryIsAfterNow) { // meta term now.l.i and fUpper must be >= queryLower String s1 = this.getMetaValue("now.l",interval); Query q1 = new TermQuery(new Term(fMeta,s1)); Query q2 = NumericRangeQuery.newLongRange( fUpper,nStep,queryLower,null,true,true); bqNow.add(q1,BooleanClause.Occur.MUST); bqNow.add(q2,BooleanClause.Occur.MUST); } BooleanQuery bq = new BooleanQuery(); bq.add(bqDeterminate,BooleanClause.Occur.SHOULD); bq.add(bqNow,BooleanClause.Occur.SHOULD); return bq; } /** * Constructs a query for documents that are not * equal to the input time period. * @return the query */ private Query makeNotEquals() { Query qEquals = this.makeEquals(); BooleanQuery qNotEquals = new BooleanQuery(); qNotEquals.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD); qNotEquals.add(qEquals,BooleanClause.Occur.MUST_NOT); return qNotEquals; } /** * Constructs a query for documents that have a null time period. * @return the query */ private Query makeNull() { int nStep = this.precisionStep; Query qHasIntervals = NumericRangeQuery.newLongRange( this.multiplicityFieldName,nStep,1L,null,true,true); BooleanQuery qNull = new BooleanQuery(); qNull.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD); qNull.add(qHasIntervals,BooleanClause.Occur.MUST_NOT); return qNull; } /** * Constructs a query for documents that are within the input time period. * @return the query */ private Query makeWithin() { BooleanQuery bq = new BooleanQuery(); Query q = this.makeWithinInterval(0); bq.add(q,BooleanClause.Occur.MUST); return bq; } /** * Constructs a query for a document interval that is within * the input time period. * @param interval the field name index for the interval * @return the query */ private Query makeWithinInterval(int interval) { // Within: docMin >= qryMin AND docMax <= qryMax int nStep = this.precisionStep; String fMeta = this.intervalMetaFieldName; String fLower = this.getLowerFieldName(interval); String fUpper = this.getUpperFieldName(interval); Query qDocLowerWithin = NumericRangeQuery.newLongRange( fLower,nStep,queryLower,queryUpper,true,true); Query qDocUpperWithin = NumericRangeQuery.newLongRange( fUpper,nStep,queryLower,queryUpper,true,true); BooleanQuery qIntervalWithin = new BooleanQuery(); qIntervalWithin.add(qDocLowerWithin,BooleanClause.Occur.MUST); qIntervalWithin.add(qDocUpperWithin,BooleanClause.Occur.MUST); String sMeta = this.getMetaValue("determinate",interval); Query qIsDeterminate = new TermQuery(new Term(fMeta,sMeta)); BooleanQuery bqDeterminate = new BooleanQuery(); bqDeterminate.add(qIsDeterminate,BooleanClause.Occur.MUST); bqDeterminate.add(qIntervalWithin,BooleanClause.Occur.MUST); // intervals that intersect now BooleanQuery bqNow = null; if (this.queryIntersectsNow) { // meta term now.i and is within String s1 = this.getMetaValue("now",interval); Query q1 = new TermQuery(new Term(fMeta,s1)); // meta term now.l.i and fUpper must be <= queryUpper String s2 = this.getMetaValue("now.l",interval); Query qM2 = new TermQuery(new Term(fMeta,s2)); Query qI2 = NumericRangeQuery.newLongRange( fUpper,nStep,null,queryUpper,true,true); BooleanQuery q2 = new BooleanQuery(); q2.add(qM2,BooleanClause.Occur.MUST); q2.add(qI2,BooleanClause.Occur.MUST); // meta term now.u.i and fLower must be >= queryLower String s3 = this.getMetaValue("now.u",interval); Query qM3 = new TermQuery(new Term(fMeta,s3)); Query qI3 = NumericRangeQuery.newLongRange( fLower,nStep,queryLower,null,true,true); BooleanQuery q3 = new BooleanQuery(); q3.add(qM3,BooleanClause.Occur.MUST); q3.add(qI3,BooleanClause.Occur.MUST); bqNow = new BooleanQuery(); bqNow.add(q1,BooleanClause.Occur.SHOULD); bqNow.add(q2,BooleanClause.Occur.SHOULD); bqNow.add(q3,BooleanClause.Occur.SHOULD); } else if (this.queryIsBeforeNow) { // not within } else if (this.queryIsAfterNow) { // not within } if (bqNow == null) { return bqDeterminate; } else { BooleanQuery bq = new BooleanQuery(); bq.add(bqDeterminate,BooleanClause.Occur.SHOULD); bq.add(bqNow,BooleanClause.Occur.SHOULD); return bq; } } /** * Parses a date/time string. * @param dateTime the date/time * @param isUpper true if this is an upper boundary * @return the corresponding time in millis * @throws IllegalArgumentException if the input does not conform */ private Long parseDateTime(String dateTime, boolean isUpper) { dateTime = Val.chkStr(dateTime); String lc = dateTime.toLowerCase(); if (lc.equals("*")) { return null; } else if (lc.equals("now") || lc.equals("present")) { return new Long(this.now); } else if (lc.equals("unknown")) { return null; } else { Calendar calendar = null; String s = dateTime; if (s.startsWith("-")) s = s.substring(1); if (s.length() >= "1000000000".length()) { boolean bChkMillis = true; char[] ca = s.toCharArray(); for (char c: ca) { if (!Character.isDigit(c)) { bChkMillis = false; break; } } if (bChkMillis) { try { long l = Long.valueOf(dateTime); calendar = new GregorianCalendar(); calendar.setTimeInMillis(l); } catch (NumberFormatException nfe) { calendar = null; } } } if (calendar == null) { calendar = TpUtil.parseIsoDateTime(dateTime); } if (isUpper) { TpUtil.advanceToUpperBoundary(calendar,dateTime); } if (LOGGER.isLoggable(Level.FINER)) { String sMsg = dateTime+" -> "+calendar.getTimeInMillis()+" "+ TpUtil.printIsoDateTime(calendar); LOGGER.finer(sMsg); } return new Long(calendar.getTimeInMillis()); } } }