blob: 69a4b4a3bb0be19263acda591f597f8e256a7fe9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011 Tasktop Technologies.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Tasktop Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.tasks.index.core;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.DateTools.Resolution;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.NIOFSDirectory;
import org.apache.lucene.util.Version;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.ITaskListChangeListener;
import org.eclipse.mylyn.internal.tasks.core.ITaskListRunnable;
import org.eclipse.mylyn.internal.tasks.core.TaskContainerDelta;
import org.eclipse.mylyn.internal.tasks.core.TaskList;
import org.eclipse.mylyn.internal.tasks.core.data.ITaskDataManagerListener;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManagerEvent;
import org.eclipse.mylyn.tasks.core.IRepositoryElement;
import org.eclipse.mylyn.tasks.core.IRepositoryPerson;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskCommentMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
/**
* An index on a task list.
*
* @author David Green
*/
public class TaskListIndex implements ITaskDataManagerListener, ITaskListChangeListener {
public abstract static class TaskCollector {
public abstract void collect(ITask task);
}
private static final Object COMMAND_RESET_INDEX = "index:reset"; //$NON-NLS-1$
public static enum IndexField {
IDENTIFIER(false, null, false), //
TASK_KEY(false, null, false), //
SUMMARY(true, null, false), //
CONTENT(true, null, false), //
ASSIGNEE(true, TaskAttribute.USER_ASSIGNED, false), //
REPORTER(true, TaskAttribute.USER_REPORTER, false), //
PERSON(true, null, false), //
COMPONENT(true, TaskAttribute.COMPONENT, false), //
COMPLETION_DATE(true, null, true), //
CREATION_DATE(true, null, true), //
DUE_DATE(true, null, true), //
MODIFICATION_DATE(true, null, true), //
DESCRIPTION(true, TaskAttribute.DESCRIPTION, false), //
KEYWORDS(true, TaskAttribute.KEYWORDS, false), //
PRODUCT(true, TaskAttribute.PRODUCT, false), //
RESOLUTION(true, TaskAttribute.RESOLUTION, false), //
SEVERITY(true, TaskAttribute.SEVERITY, false), //
STATUS(true, TaskAttribute.STATUS, false);
private final boolean userVisible;
private final String attributeId;
private final boolean dateTime;
private IndexField(boolean userVisible, String attributeId, boolean dateTime) {
this.userVisible = userVisible;
this.attributeId = attributeId;
this.dateTime = dateTime;
}
public String fieldName() {
return name().toLowerCase();
}
/**
* get the task attribute id, or null if this field has special handling
*/
public String getAttributeId() {
return attributeId;
}
/**
* indicate if the field should be exposed in the UI
*/
public boolean isUserVisible() {
return userVisible;
}
/**
* indicate if the field is a date/time field
*/
public boolean isDateTime() {
return dateTime;
}
public static IndexField fromFieldName(String fieldName) {
try {
return IndexField.valueOf(fieldName.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
public boolean isPersonField() {
return this == PERSON || this == REPORTER || this == ASSIGNEE;
}
}
private static enum MaintainIndexType {
STARTUP, REINDEX
}
private Directory directory;
private MaintainIndexJob maintainIndexJob;
private final Map<ITask, TaskData> reindexQueue = new HashMap<ITask, TaskData>();
private IndexReader indexReader;
private boolean rebuildIndex = false;
private String lastPatternString;
private Set<String> lastResults;
private IndexField defaultField = IndexField.SUMMARY;
private final TaskList taskList;
private final TaskDataManager dataManager;
private long startupDelay = 6000L;
private long reindexDelay = 3000L;
private int maxMatchSearchHits = 1500;
private TaskListIndex(TaskList taskList, TaskDataManager dataManager) {
if (taskList == null) {
throw new IllegalArgumentException();
}
if (dataManager == null) {
throw new IllegalArgumentException();
}
this.taskList = taskList;
this.dataManager = dataManager;
}
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, File indexLocation) {
this(taskList, dataManager, indexLocation, 6000L);
}
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, File indexLocation, long startupDelay) {
this(taskList, dataManager);
if (startupDelay < 0L || startupDelay > (1000L * 60)) {
throw new IllegalArgumentException();
}
if (indexLocation == null) {
throw new IllegalArgumentException();
}
this.startupDelay = startupDelay;
if (!indexLocation.exists()) {
rebuildIndex = true;
indexLocation.mkdirs();
}
if (indexLocation.exists() && indexLocation.isDirectory()) {
try {
Logger.getLogger(TaskListIndex.class.getName()).fine("task list index: " + indexLocation); //$NON-NLS-1$
directory = new NIOFSDirectory(indexLocation);
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.BUNDLE_ID,
"Cannot create task list index", e)); //$NON-NLS-1$
}
}
initialize();
}
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, Directory directory) {
this(taskList, dataManager);
this.directory = directory;
initialize();
}
public long getReindexDelay() {
return reindexDelay;
}
public void setReindexDelay(long reindexDelay) {
this.reindexDelay = reindexDelay;
}
public IndexField getDefaultField() {
return defaultField;
}
public void setDefaultField(IndexField defaultField) {
this.defaultField = defaultField;
lastResults = null;
}
public int getMaxMatchSearchHits() {
return maxMatchSearchHits;
}
public void setMaxMatchSearchHits(int maxMatchSearchHits) {
this.maxMatchSearchHits = maxMatchSearchHits;
}
private void initialize() {
if (!rebuildIndex) {
IndexReader indexReader = null;
try {
indexReader = getIndexReader();
} catch (Exception e) {
// ignore, this can happen if the index is corrupt
}
if (indexReader == null) {
rebuildIndex = true;
}
}
maintainIndexJob = new MaintainIndexJob();
dataManager.addListener(this);
taskList.addChangeListener(this);
scheduleIndexMaintenance(MaintainIndexType.STARTUP);
}
private void scheduleIndexMaintenance(MaintainIndexType type) {
long delay = 0L;
switch (type) {
case STARTUP:
delay = startupDelay;
break;
case REINDEX:
delay = reindexDelay;
}
if (delay == 0L) {
// primarily for testing purposes
maintainIndexJob.cancel();
try {
maintainIndexJob.join();
} catch (InterruptedException e) {
// ignore
}
maintainIndexJob.run(new NullProgressMonitor());
} else {
maintainIndexJob.schedule(delay);
}
}
public boolean matches(ITask task, String patternString) {
if (patternString.equals(COMMAND_RESET_INDEX)) {
reindex();
}
IndexReader indexReader = getIndexReader();
if (indexReader != null) {
Set<String> hits;
synchronized (indexReader) {
if (lastResults == null || (lastPatternString == null || !lastPatternString.equals(patternString))) {
this.lastPatternString = patternString;
long startTime = System.currentTimeMillis();
hits = new HashSet<String>();
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
try {
Query query = computeQuery(patternString);
TopDocs results = indexSearcher.search(query, maxMatchSearchHits);
for (ScoreDoc scoreDoc : results.scoreDocs) {
Document document = indexReader.document(scoreDoc.doc);
hits.add(document.get(IndexField.IDENTIFIER.fieldName()));
}
lastResults = hits;
} catch (IOException e) {
StatusHandler.fail(new Status(IStatus.ERROR, TasksIndexCore.BUNDLE_ID,
"Unexpected failure within task list index", e)); //$NON-NLS-1$
} finally {
try {
indexSearcher.close();
} catch (IOException e) {
// ignore
}
}
Logger.getLogger(TaskListIndex.class.getName()).fine(
"New query in " + (System.currentTimeMillis() - startTime) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
hits = lastResults;
}
}
String taskIdentifier = task.getHandleIdentifier();
return hits != null && hits.contains(taskIdentifier);
}
return false;
}
public void reindex() {
rebuildIndex = true;
scheduleIndexMaintenance(MaintainIndexType.REINDEX);
}
/**
* call to wait until index maintenance has completed
*
* @throws InterruptedException
*/
public void waitUntilIdle() throws InterruptedException {
if (!Platform.isRunning() && reindexDelay != 0L) {
// job join() behaviour is not the same when platform is not running
Logger.getLogger(TaskListIndex.class.getName()).warning(
"Index job joining may not work properly when Eclipse platform is not running"); //$NON-NLS-1$
}
maintainIndexJob.join();
}
public void find(String patternString, TaskCollector collector, int resultsLimit) {
IndexReader indexReader = getIndexReader();
if (indexReader != null) {
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
try {
Query query = computeQuery(patternString);
TopDocs results = indexSearcher.search(query, resultsLimit);
for (ScoreDoc scoreDoc : results.scoreDocs) {
Document document = indexReader.document(scoreDoc.doc);
String taskIdentifier = document.get(IndexField.IDENTIFIER.fieldName());
AbstractTask task = taskList.getTask(taskIdentifier);
if (task != null) {
collector.collect(task);
}
}
} catch (IOException e) {
StatusHandler.fail(new Status(IStatus.ERROR, TasksIndexCore.BUNDLE_ID,
"Unexpected failure within task list index", e)); //$NON-NLS-1$
} finally {
try {
indexSearcher.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private Query computeQuery(String patternString) {
String upperPatternString = patternString.toUpperCase();
boolean hasBooleanSpecifiers = upperPatternString.contains(" OR ") || upperPatternString.contains(" AND ") //$NON-NLS-1$ //$NON-NLS-2$
|| upperPatternString.contains(" NOT "); //$NON-NLS-1$
if (patternString.indexOf(':') == -1 && !hasBooleanSpecifiers && defaultField == IndexField.SUMMARY
&& patternString.indexOf('"') == -1) {
return new PrefixQuery(new Term(defaultField.fieldName(), patternString));
}
QueryParser qp = new QueryParser(Version.LUCENE_CURRENT, defaultField.fieldName(), new StandardAnalyzer(
Version.LUCENE_CURRENT));
Query q;
try {
q = qp.parse(patternString);
} catch (ParseException e) {
return new PrefixQuery(new Term(defaultField.fieldName(), patternString));
}
// relax term clauses to be prefix clauses so that we get results close
// to what we're expecting
// from previous task list search
if (q instanceof BooleanQuery) {
BooleanQuery query = (BooleanQuery) q;
for (BooleanClause clause : query.getClauses()) {
if (clause.getQuery() instanceof TermQuery) {
TermQuery termQuery = (TermQuery) clause.getQuery();
clause.setQuery(new PrefixQuery(termQuery.getTerm()));
}
if (!hasBooleanSpecifiers) {
clause.setOccur(Occur.MUST);
}
}
} else if (q instanceof TermQuery) {
return new PrefixQuery(((TermQuery) q).getTerm());
}
return q;
}
public void close() {
dataManager.removeListener(this);
taskList.removeChangeListener(this);
maintainIndexJob.cancel();
try {
maintainIndexJob.join();
} catch (InterruptedException e) {
// ignore
}
synchronized (this) {
if (indexReader != null) {
synchronized (indexReader) {
try {
indexReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
indexReader = null;
}
}
try {
directory.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private IndexReader getIndexReader() {
try {
synchronized (this) {
if (indexReader == null) {
indexReader = IndexReader.open(directory, true);
}
return indexReader;
}
} catch (CorruptIndexException e) {
rebuildIndex = true;
if (maintainIndexJob != null) {
scheduleIndexMaintenance(MaintainIndexType.REINDEX);
}
} catch (FileNotFoundException e) {
rebuildIndex = true;
// expected if the index doesn't exist
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public void taskDataUpdated(TaskDataManagerEvent event) {
reindex(event.getTask(), event.getTaskData());
}
public void editsDiscarded(TaskDataManagerEvent event) {
reindex(event.getTask(), event.getTaskData());
}
public void containersChanged(Set<TaskContainerDelta> containers) {
for (TaskContainerDelta delta : containers) {
switch (delta.getKind()) {
case ADDED:
case REMOVED:
case CONTENT:
IRepositoryElement element = delta.getElement();
if (element instanceof ITask) {
ITask task = (ITask) element;
if ("local".equals(((AbstractTask) task).getConnectorKind())) { //$NON-NLS-1$
reindex(task, null);
}
}
}
}
}
private void reindex(ITask task, TaskData taskData) {
if (task == null) {
// this can happen when edits are discarded
return;
}
synchronized (reindexQueue) {
reindexQueue.put(task, taskData);
}
scheduleIndexMaintenance(MaintainIndexType.REINDEX);
}
private void addIndexedAttributes(Document document, ITask task, TaskAttribute root) {
addIndexedAttribute(document, IndexField.SUMMARY, root.getMappedAttribute(TaskAttribute.SUMMARY));
addIndexedAttribute(document, IndexField.TASK_KEY, task.getTaskKey());
addIndexedAttribute(document, IndexField.CONTENT, root.getMappedAttribute(TaskAttribute.SUMMARY));
addIndexedAttribute(document, IndexField.CONTENT, root.getMappedAttribute(TaskAttribute.DESCRIPTION));
addIndexedAttribute(document, IndexField.CONTENT, root.getAttribute("status_whiteboard")); //$NON-NLS-1$
addIndexedDateAttributes(document, task);
List<TaskAttribute> commentAttributes = root.getTaskData()
.getAttributeMapper()
.getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_COMMENT);
for (TaskAttribute commentAttribute : commentAttributes) {
TaskCommentMapper commentMapper = TaskCommentMapper.createFrom(commentAttribute);
String text = commentMapper.getText();
if (text.length() != 0) {
addIndexedAttribute(document, IndexField.CONTENT, text);
}
IRepositoryPerson author = commentMapper.getAuthor();
if (author != null) {
addIndexedAttribute(document, IndexField.PERSON, author.getPersonId());
}
}
List<TaskAttribute> personAttributes = root.getTaskData()
.getAttributeMapper()
.getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_PERSON);
for (TaskAttribute personAttribute : personAttributes) {
addIndexedAttribute(document, IndexField.PERSON, personAttribute);
}
for (IndexField field : IndexField.values()) {
if (field.getAttributeId() != null) {
addIndexedAttribute(document, field, root.getMappedAttribute(field.getAttributeId()));
}
}
}
private void addIndexedAttributes(Document document, ITask task) {
addIndexedAttribute(document, IndexField.SUMMARY, task.getSummary());
addIndexedAttribute(document, IndexField.TASK_KEY, task.getTaskKey());
addIndexedAttribute(document, IndexField.CONTENT, task.getSummary());
addIndexedAttribute(document, IndexField.CONTENT, ((AbstractTask) task).getNotes());
addIndexedDateAttributes(document, task);
}
private void addIndexedDateAttributes(Document document, ITask task) {
addIndexedAttribute(document, IndexField.COMPLETION_DATE, task.getCompletionDate());
addIndexedAttribute(document, IndexField.CREATION_DATE, task.getCreationDate());
addIndexedAttribute(document, IndexField.DUE_DATE, task.getDueDate());
addIndexedAttribute(document, IndexField.MODIFICATION_DATE, task.getModificationDate());
}
private void addIndexedAttribute(Document document, IndexField indexField, TaskAttribute attribute) {
if (attribute == null) {
return;
}
// if (indexField == IndexField.ASSIGNEE) {
// System.out.println(indexField + "=" + attribute.getValue());
// }
List<String> values = attribute.getValues();
for (String value : values) {
if (value.length() != 0) {
addIndexedAttribute(document, indexField, value);
}
}
}
private void addIndexedAttribute(Document document, IndexField indexField, String value) {
if (value == null) {
return;
}
Field field = document.getField(indexField.fieldName());
if (field == null) {
field = new Field(indexField.fieldName(), value, Store.YES, org.apache.lucene.document.Field.Index.ANALYZED);
document.add(field);
} else {
String existingValue = field.stringValue();
if (indexField != IndexField.PERSON || !existingValue.contains(value)) {
field.setValue(existingValue + " " + value); //$NON-NLS-1$
}
}
}
private void addIndexedAttribute(Document document, IndexField indexField, Date date) {
if (date == null) {
return;
}
// FIXME: date tools converts dates to GMT, and we don't really want that. So
// move the date by the GMT offset if there is any
String value = DateTools.dateToString(date, Resolution.HOUR);
Field field = document.getField(indexField.fieldName());
if (field == null) {
field = new Field(indexField.fieldName(), value, Store.YES, org.apache.lucene.document.Field.Index.ANALYZED);
document.add(field);
} else {
field.setValue(value);
}
}
private class MaintainIndexJob extends Job {
public MaintainIndexJob() {
super(Messages.TaskListIndex_indexerJob);
setUser(false);
setSystem(false); // true?
setPriority(Job.LONG);
}
@Override
public IStatus run(IProgressMonitor m) {
final int WORK_PER_SEGMENT = 1000;
SubMonitor monitor = SubMonitor.convert(m, 3 * WORK_PER_SEGMENT);
try {
try {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
if (!rebuildIndex) {
try {
IndexReader reader = IndexReader.open(directory, false);
reader.close();
} catch (CorruptIndexException e) {
rebuildIndex = true;
}
}
if (rebuildIndex) {
synchronized (reindexQueue) {
reindexQueue.clear();
}
SubMonitor reindexMonitor = monitor.newChild(WORK_PER_SEGMENT);
final IndexWriter writer = new IndexWriter(directory, new TaskAnalyzer(), true,
IndexWriter.MaxFieldLength.UNLIMITED);
try {
final List<ITask> allTasks = new ArrayList<ITask>(5000);
taskList.run(new ITaskListRunnable() {
public void execute(IProgressMonitor monitor) throws CoreException {
allTasks.addAll(taskList.getAllTasks());
}
}, monitor.newChild(1));
int reindexErrorCount = 0;
reindexMonitor.beginTask(Messages.TaskListIndex_task_rebuildingIndex, allTasks.size());
for (ITask task : allTasks) {
try {
TaskData taskData = dataManager.getTaskData(task);
add(writer, task, taskData);
reindexMonitor.worked(1);
} catch (CoreException e) {
// an individual task data error should not prevent the index from updating
// but don't flood the log in the case of multiple errors
if (reindexErrorCount++ == 0) {
StatusHandler.log(e.getStatus());
}
} catch (IOException e) {
throw e;
}
}
synchronized (TaskListIndex.this) {
rebuildIndex = false;
}
} finally {
writer.close();
reindexMonitor.done();
}
} else {
monitor.worked(WORK_PER_SEGMENT);
}
for (;;) {
synchronized (reindexQueue) {
if (reindexQueue.isEmpty()) {
break;
}
}
Map<ITask, TaskData> queue = new HashMap<ITask, TaskData>();
IndexReader reader = IndexReader.open(directory, false);
try {
synchronized (reindexQueue) {
queue.putAll(reindexQueue);
for (ITask task : queue.keySet()) {
reindexQueue.remove(task);
}
}
Iterator<Entry<ITask, TaskData>> it = queue.entrySet().iterator();
while (it.hasNext()) {
Entry<ITask, TaskData> entry = it.next();
reader.deleteDocuments(new Term(IndexField.IDENTIFIER.fieldName(), entry.getKey()
.getHandleIdentifier()));
}
} finally {
reader.close();
}
monitor.worked(WORK_PER_SEGMENT);
IndexWriter writer = new IndexWriter(directory, new TaskAnalyzer(), false,
IndexWriter.MaxFieldLength.UNLIMITED);
try {
for (Entry<ITask, TaskData> entry : queue.entrySet()) {
ITask task = entry.getKey();
TaskData taskData = entry.getValue();
add(writer, task, taskData);
}
} finally {
writer.close();
}
monitor.worked(WORK_PER_SEGMENT);
}
synchronized (TaskListIndex.this) {
indexReader = null;
}
} catch (CoreException e) {
throw e;
} catch (Throwable e) {
throw new CoreException(new Status(IStatus.ERROR, TasksIndexCore.BUNDLE_ID,
"Unexpected exception: " + e.getMessage(), e)); //$NON-NLS-1$
}
} catch (CoreException e) {
MultiStatus logStatus = new MultiStatus(TasksIndexCore.BUNDLE_ID, 0,
"Failed to update task list index", e); //$NON-NLS-1$
logStatus.add(e.getStatus());
StatusHandler.log(logStatus);
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
/**
* @param writer
* @param task
* the task
* @param taskData
* may be null for local tasks
* @throws CorruptIndexException
* @throws IOException
*/
private void add(IndexWriter writer, ITask task, TaskData taskData) throws CorruptIndexException, IOException {
Document document = new Document();
document.add(new Field(IndexField.IDENTIFIER.fieldName(), task.getHandleIdentifier(), Store.YES,
org.apache.lucene.document.Field.Index.ANALYZED));
if (taskData == null) {
if ("local".equals(((AbstractTask) task).getConnectorKind())) { //$NON-NLS-1$
addIndexedAttributes(document, task);
} else {
return;
}
} else {
addIndexedAttributes(document, task, taskData.getRoot());
}
writer.addDocument(document);
}
}
/**
* Computes a query element for a field that must lie in a specified date range.
*
* @param field
* the field
* @param lowerBoundInclusive
* the date lower bound that the field value must match, inclusive
* @param upperBoundInclusive
* the date upper bound that the field value must match, inclusive
* @return
*/
public String computeQueryFieldDateRange(IndexField field, Date lowerBoundInclusive, Date upperBoundInclusive) {
return field.fieldName()
+ ":[" + DateTools.dateToString(lowerBoundInclusive, Resolution.DAY) + " TO " + DateTools.dateToString(upperBoundInclusive, Resolution.DAY) + "]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
}
}