blob: d808f22aa2e7276de8866c70f200a325c5908688 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2015 Tasktop Technologies.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Tasktop Technologies - initial API and implementation
* Red Hat, Inc. Bug 384685 - consume Apache Lucene 3.x
*******************************************************************************/
package org.eclipse.mylyn.internal.tasks.index.core;
import static org.eclipse.mylyn.tasks.core.data.TaskAttribute.META_INDEXED_AS_CONTENT;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Logger;
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.Store;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexFormatTooOldException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.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.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.NIOFSDirectory;
import org.apache.lucene.util.InfoStream;
import org.eclipse.core.runtime.Assert;
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.ITaskList;
import org.eclipse.mylyn.internal.tasks.core.ITaskListChangeListener;
import org.eclipse.mylyn.internal.tasks.core.ITaskListRunnable;
import org.eclipse.mylyn.internal.tasks.core.TaskAttachment;
import org.eclipse.mylyn.internal.tasks.core.TaskComment;
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.IRepositoryListener;
import org.eclipse.mylyn.tasks.core.IRepositoryManager;
import org.eclipse.mylyn.tasks.core.IRepositoryPerson;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema;
import org.eclipse.mylyn.tasks.core.data.DefaultTaskSchema;
import org.eclipse.mylyn.tasks.core.data.ITaskDataManager;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskData;
/**
* An index on a task list, provides a way to {@link #find(String, TaskCollector, int) search for tasks}, and a way to
* {@link #matches(ITask, String) match tasks}. Tasks are matched against a search query.
* <p>
* The task list has a configurable delay before it updates, meaning that there is a period of time where the index will
* be out of date with respect to task changes. The idea is that updates to the index can be "batched" for greater
* efficiency. Additionally, it's possible for a task to be updated either before or after the index is added to the
* task list as a listener, thus opening the possibility of changes without updates to the index. In either of these
* cases, the index can be out of date with respect to the current state of tasks. If the index is used in such a state,
* the result could be either false matches, no match where there should be a match, or incorrect prioritization of
* index "hits".
* </p>
* <p>
* The index has the option of reindexing all tasks via API. This will bring the index up to date and is useful for
* cases where it's known that the index may not be up to date. In its current form this reindex operation can be
* triggered by the user by including "index:reset" in the search string. Reindexing is potentially an expensive, IO
* intensive long-running operation. With about 20,000 tasks in my task list and an SSD, reindexing takes about 90
* seconds.
* </p>
*
* @author David Green
* @author Steffen Pingel
*/
public class TaskListIndex implements ITaskDataManagerListener, ITaskListChangeListener, IRepositoryListener {
private static final Object COMMAND_RESET_INDEX = "index:reset"; //$NON-NLS-1$
private static final String INDEX_TASK_ATTRIBUTE_PREFIX = "index:"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_IDENTIFIER = INDEX_TASK_ATTRIBUTE_PREFIX + "handle-identifier"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_REPOSITORY_URL = INDEX_TASK_ATTRIBUTE_PREFIX + "repository-url"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_CONTENT = INDEX_TASK_ATTRIBUTE_PREFIX + "content"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_PERSON = INDEX_TASK_ATTRIBUTE_PREFIX + "person"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_ATTACHMENT_NAME = INDEX_TASK_ATTRIBUTE_PREFIX + "attachment"; //$NON-NLS-1$
private static final String TASK_ATTRIBUTE_NOTES = INDEX_TASK_ATTRIBUTE_PREFIX + "notes"; //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_IDENTIFIER = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_IDENTIFIER, Messages.TaskListIndex_field_identifier, TaskAttribute.TYPE_SHORT_TEXT,
"identifier"); //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_REPOSITORY_URL = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_REPOSITORY_URL, Messages.TaskListIndex_field_repository_url, TaskAttribute.TYPE_URL,
"repository_url"); //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_CONTENT = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_CONTENT, Messages.TaskListIndex_field_content, TaskAttribute.TYPE_LONG_TEXT, "content"); //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_PERSON = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_PERSON, Messages.TaskListIndex_field_person, TaskAttribute.TYPE_PERSON, "person"); //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_TASK_KEY = DefaultTaskSchema
.getInstance().TASK_KEY;
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_SUMMARY = DefaultTaskSchema
.getInstance().SUMMARY;
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_ATTACHMENT_NAME = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_ATTACHMENT_NAME, Messages.TaskListIndex_field_attachment, TaskAttribute.TYPE_SHORT_TEXT,
"attachment"); //$NON-NLS-1$
public static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_NOTES = new AbstractTaskSchema.Field(
TASK_ATTRIBUTE_NOTES, Messages.TaskListIndex_field_notes, TaskAttribute.TYPE_LONG_TEXT, "notes"); //$NON-NLS-1$
private class MaintainIndexJob extends Job {
public MaintainIndexJob() {
super(Messages.TaskListIndex_indexerJob);
setUser(false);
setSystem(true);
setPriority(Job.LONG);
}
@Override
public IStatus run(IProgressMonitor m) {
if (m.isCanceled()) {
return Status.CANCEL_STATUS;
}
try {
maintainIndex(m);
} catch (CoreException e) {
MultiStatus logStatus = new MultiStatus(TasksIndexCore.ID_PLUGIN, 0, "Failed to update task list index", //$NON-NLS-1$
e);
logStatus.add(e.getStatus());
StatusHandler.log(logStatus);
}
return Status.OK_STATUS;
}
}
public abstract static class TaskCollector {
public abstract void collect(ITask task);
}
/**
* keeps track of fields that are handled specially in the code so that we don't use the generalized field handling
* for indexing them.
*/
private final Set<AbstractTaskSchema.Field> specialFields = new HashSet<AbstractTaskSchema.Field>();
private final Set<AbstractTaskSchema.Field> indexedFields = new LinkedHashSet<AbstractTaskSchema.Field>();
{
specialFields.add(FIELD_IDENTIFIER);
specialFields.add(FIELD_REPOSITORY_URL);
specialFields.add(FIELD_CONTENT);
specialFields.add(FIELD_PERSON);
specialFields.add(FIELD_TASK_KEY);
specialFields.add(FIELD_ATTACHMENT_NAME);
specialFields.add(FIELD_NOTES);
addIndexedField(FIELD_IDENTIFIER);
addIndexedField(FIELD_TASK_KEY);
addIndexedField(FIELD_REPOSITORY_URL);
addIndexedField(FIELD_SUMMARY);
addIndexedField(FIELD_CONTENT);
addIndexedField(FIELD_ATTACHMENT_NAME);
addIndexedField(DefaultTaskSchema.getInstance().USER_ASSIGNED);
addIndexedField(DefaultTaskSchema.getInstance().USER_REPORTER);
addIndexedField(FIELD_PERSON);
addIndexedField(DefaultTaskSchema.getInstance().COMPONENT);
addIndexedField(DefaultTaskSchema.getInstance().DATE_COMPLETION);
addIndexedField(DefaultTaskSchema.getInstance().DATE_CREATION);
addIndexedField(DefaultTaskSchema.getInstance().DATE_DUE);
addIndexedField(DefaultTaskSchema.getInstance().DATE_MODIFICATION);
addIndexedField(DefaultTaskSchema.getInstance().DESCRIPTION);
addIndexedField(DefaultTaskSchema.getInstance().KEYWORDS);
addIndexedField(DefaultTaskSchema.getInstance().PRODUCT);
addIndexedField(DefaultTaskSchema.getInstance().RESOLUTION);
addIndexedField(DefaultTaskSchema.getInstance().SEVERITY);
addIndexedField(DefaultTaskSchema.getInstance().STATUS);
addIndexedField(FIELD_NOTES);
}
private static enum MaintainIndexType {
STARTUP, REINDEX
}
private Directory directory;
private MaintainIndexJob maintainIndexJob;
/**
* must be synchronized before accessing or modifying
*/
private final Map<ITask, TaskData> reindexQueue = new HashMap<ITask, TaskData>();
/**
* do not access directly, instead use {@link #getIndexReader()}. 'this' must be synchronized before accessing or
* modifying
*/
private IndexReader indexReader;
/**
* indicate the need to rebuild the whole index
*/
private volatile boolean rebuildIndex = false;
/**
* 'this' must be synchronized before accessing or modifying
*/
private String lastPatternString;
/**
* 'this' must be synchronized before accessing or modifying
*/
private Set<String> lastResults;
private AbstractTaskSchema.Field defaultField = FIELD_SUMMARY;
private final TaskList taskList;
private final TaskDataManager dataManager;
private final IRepositoryManager repositoryManager;
private long startupDelay = 6000L;
private long reindexDelay = 3000L;
private int maxMatchSearchHits = 1500;
/**
* must hold this lock as a read lock when accessing the index, and must hold this lock as a write lock when closing
* or reassigning {@link #indexReader}.
*/
private final ReadWriteLock indexReaderLock = new ReentrantReadWriteLock(true);
private TaskListIndex(TaskList taskList, TaskDataManager dataManager, IRepositoryManager repositoryManager) {
Assert.isNotNull(taskList);
Assert.isNotNull(dataManager);
Assert.isNotNull(repositoryManager);
this.taskList = taskList;
this.dataManager = dataManager;
this.repositoryManager = repositoryManager;
}
private void addIndexedField(org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field field) {
Assert.isNotNull(field);
Assert.isNotNull(field.getIndexKey());
indexedFields.add(field);
}
/**
* the task list associated with this index
*/
public ITaskList getTaskList() {
return taskList;
}
/**
* the data manager associated with this index
*/
public ITaskDataManager getDataManager() {
return dataManager;
}
/**
* the repository manager associated with this index
*/
public IRepositoryManager getRepositoryManager() {
return repositoryManager;
}
/**
* Create an index on the given task list. Must be matched by a corresponding call to {@link #close()}.
*
* @param taskList
* the task list that is to be indexed
* @param dataManager
* the data manager that corresponds to the task list
* @param repositoryManager
* the repository manager that corresponds to the task list
* @param indexLocation
* the location of the index on the filesystem
* @see #TaskListIndex(TaskList, TaskDataManager, Directory)
*/
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, IRepositoryManager repositoryManager,
File indexLocation) {
this(taskList, dataManager, repositoryManager, indexLocation, 6000L);
}
/**
* Create an index on the given task list. Must be matched by a corresponding call to {@link #close()}.
*
* @param taskList
* the task list that is to be indexed
* @param dataManager
* the data manager that corresponds to the task list
* @param repositoryManager
* the repository manager that corresponds to the task list
* @param startupDelay
* the delay in miliseconds before the index initialization maintenance process should begin
* @see #TaskListIndex(TaskList, TaskDataManager, File)
*/
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, IRepositoryManager repositoryManager,
File indexLocation, long startupDelay) {
this(taskList, dataManager, repositoryManager);
Assert.isTrue(startupDelay >= 0L && startupDelay <= (1000L * 60));
Assert.isNotNull(indexLocation);
this.startupDelay = startupDelay;
setLocationInternal(indexLocation);
initialize();
}
/**
* Create an index on the given task list. Must be matched by a corresponding call to {@link #close()}.
*
* @param taskList
* the task list that is to be indexed
* @param dataManager
* the data manager that corresponds to the task list
* @param repositoryManager
* the repository manager that corresponds to the task list
* @param directory
* the directory in which the index should be stored
* @see #TaskListIndex(TaskList, TaskDataManager, File)
*/
public TaskListIndex(TaskList taskList, TaskDataManager dataManager, IRepositoryManager repositoryManager,
Directory directory) {
this(taskList, dataManager, repositoryManager);
this.directory = directory;
initialize();
}
/**
* the delay before reindexing occurs after a task has changed or after {@link #reindex()} is called
*/
public long getReindexDelay() {
return reindexDelay;
}
/**
* the delay before reindexing occurs after a task has changed or after {@link #reindex()} is called.
*
* @param reindexDelay
* The delay in miliseconds. Specify 0 to indicate no delay.
*/
public void setReindexDelay(long reindexDelay) {
Assert.isTrue(reindexDelay >= 0);
this.reindexDelay = reindexDelay;
}
public void setLocation(File indexLocation) {
try {
waitUntilIdle();
} catch (InterruptedException e1) {
// ignore
}
setLocationInternal(indexLocation);
rebuildIndex = true;
scheduleIndexMaintenance(MaintainIndexType.STARTUP);
}
private void setLocationInternal(File indexLocation) {
final boolean newLocationExists = indexLocation.exists();
if (!newLocationExists) {
if (!indexLocation.mkdirs()) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Cannot create task list index folder: " + indexLocation)); //$NON-NLS-1$
}
}
Lock writeLock = indexReaderLock.writeLock();
writeLock.lock();
try {
synchronized (this) {
if (indexReader != null) {
try {
indexReader.close();
} catch (IOException e) {
// ignore
}
indexReader = null;
}
if (indexLocation.exists() && indexLocation.isDirectory()) {
if (directory != null) {
try {
directory.close();
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Cannot close index: " + e.getMessage(), e)); //$NON-NLS-1$
}
}
try {
directory = new NIOFSDirectory(indexLocation.toPath());
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Cannot create task list index", e)); //$NON-NLS-1$
}
}
}
} finally {
writeLock.unlock();
}
}
/**
* the default field used to match tasks when unspecified in the query
*/
public AbstractTaskSchema.Field getDefaultField() {
return defaultField;
}
/**
* the default field used to match tasks when unspecified in the query
*
* @param defaultField
* the default field to use in queries, must be one of the {@link #getIndexedFields() indexed fields}.
*/
public void setDefaultField(AbstractTaskSchema.Field defaultField) {
Assert.isNotNull(defaultField);
Assert.isNotNull(defaultField.getIndexKey());
Assert.isTrue(indexedFields.contains(defaultField));
this.defaultField = defaultField;
synchronized (this) {
lastResults = null;
}
}
/**
* the fields that are indexed
*/
public Set<AbstractTaskSchema.Field> getIndexedFields() {
return Collections.unmodifiableSet(indexedFields);
}
/**
* the maximum number of search hits that should be provided when using {@link #matches(ITask, String)}
*/
public int getMaxMatchSearchHits() {
return maxMatchSearchHits;
}
/**
* the maximum number of search hits that should be provided when using {@link #matches(ITask, String)}
*/
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);
repositoryManager.addListener(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
}
try {
maintainIndex(new NullProgressMonitor());
} catch (CoreException e) {
MultiStatus logStatus = new MultiStatus(TasksIndexCore.ID_PLUGIN, 0, "Failed to update task list index", //$NON-NLS-1$
e);
logStatus.add(e.getStatus());
StatusHandler.log(logStatus);
}
} else {
maintainIndexJob.schedule(delay);
}
}
/**
* Indicates if the given task matches the given pattern string. Uses the backing index to detect a match by looking
* for tasks that match the given pattern string. The results of the search are cached such that future calls to
* this method using the same pattern string do not require use of the backing index, making this method very
* efficient for multiple calls with the same pattern string. Cached results for a given pattern string are
* discarded if this method is called with a different pattern string.
*
* @param task
* the task to match
* @param patternString
* the pattern used to detect a match
*/
public boolean matches(ITask task, String patternString) {
if (patternString.equals(COMMAND_RESET_INDEX)) {
reindex();
}
Lock readLock = indexReaderLock.readLock();
readLock.lock();
try {
IndexReader indexReader = getIndexReader();
if (indexReader != null) {
Set<String> hits;
final boolean needIndexHit;
synchronized (this) {
needIndexHit = lastResults == null
|| (lastPatternString == null || !lastPatternString.equals(patternString));
}
if (needIndexHit) {
this.lastPatternString = patternString;
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(FIELD_IDENTIFIER.getIndexKey()));
}
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Unexpected failure within task list index", e)); //$NON-NLS-1$
}
} else {
hits = lastResults;
}
synchronized (this) {
if (this.indexReader == indexReader) {
this.lastPatternString = patternString;
this.lastResults = hits;
}
}
String taskIdentifier = task.getHandleIdentifier();
return hits != null && hits.contains(taskIdentifier);
}
} finally {
readLock.unlock();
}
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();
}
/**
* finds tasks that match the given pattern string
*
* @param patternString
* the pattern string, used to match tasks
* @param collector
* the collector that receives tasks
* @param resultsLimit
* the maximum number of tasks to find. Specifying a limit enables the index to be more efficient since
* it can skip over matching tasks that do not score highly enough. Specify {@link Integer#MAX_VALUE} if
* there should be no limit.
*/
public void find(String patternString, TaskCollector collector, int resultsLimit) {
Assert.isNotNull(patternString);
Assert.isNotNull(collector);
Assert.isTrue(resultsLimit > 0);
Lock readLock = indexReaderLock.readLock();
readLock.lock();
try {
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(FIELD_IDENTIFIER.getIndexKey());
AbstractTask task = taskList.getTask(taskIdentifier);
if (task != null) {
collector.collect(task);
}
}
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Unexpected failure within task list index", e)); //$NON-NLS-1$
}
}
} finally {
readLock.unlock();
}
}
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 (!hasBooleanSpecifiers && defaultField.equals(FIELD_SUMMARY) && !containsSpecialCharacters(patternString)) {
return new PrefixQuery(new Term(defaultField.getIndexKey(), patternString));
}
QueryParser qp = new QueryParser(defaultField.getIndexKey(), TaskAnalyzer.instance());
Query q;
try {
q = qp.parse(patternString);
} catch (ParseException e) {
return new PrefixQuery(new Term(defaultField.getIndexKey(), 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) {
//Since queries and clauses are now immutable we need to rewrite q
BooleanQuery.Builder qb = new BooleanQuery.Builder();
BooleanQuery query = (BooleanQuery) q;
for (BooleanClause clause : query.clauses()) {
if (clause.getQuery() instanceof TermQuery) {
TermQuery termQuery = (TermQuery) clause.getQuery();
clause = new BooleanClause(new PrefixQuery(termQuery.getTerm()),
computeOccur(clause, hasBooleanSpecifiers));
} else if (!hasBooleanSpecifiers) {
clause = new BooleanClause(clause.getQuery(), Occur.MUST);
}
qb.add(clause);
}
q = qb.build();
} else if (q instanceof TermQuery) {
return new PrefixQuery(((TermQuery) q).getTerm());
}
return q;
}
private Occur computeOccur(BooleanClause clause, boolean hasBooleanSpecifiers) {
if (!hasBooleanSpecifiers) {
return Occur.MUST;
}
return clause.getOccur();
}
private boolean containsSpecialCharacters(String patternString) {
return patternString.indexOf(':') >= 0 || patternString.indexOf('"') >= 0 || patternString.indexOf('*') >= 0
|| patternString.indexOf('?') >= 0;
}
public void close() {
dataManager.removeListener(this);
taskList.removeChangeListener(this);
repositoryManager.removeListener(this);
maintainIndexJob.cancel();
try {
maintainIndexJob.join();
} catch (InterruptedException e) {
// ignore
}
Lock writeLock = indexReaderLock.writeLock();
writeLock.lock();
try {
synchronized (this) {
if (indexReader != null) {
try {
indexReader.close();
} catch (IOException e) {
// ignore
}
indexReader = null;
}
}
if (directory != null) {
try {
directory.close();
} catch (IOException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Cannot close index: " + e.getMessage(), e)); //$NON-NLS-1$
}
}
} finally {
writeLock.unlock();
}
}
private IndexReader getIndexReader() {
try {
synchronized (this) {
if (indexReader == null) {
indexReader = DirectoryReader.open(directory);
lastResults = null;
}
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) {
// ignore
}
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);
}
}
}
}
}
/**
* advanced usage: cause the given task to be reindexed using {@link MaintainIndexType#REINDEX reindex scheduling
* rule}.
*
* @param task
* the task
* @param taskData
* the task data, or nul if it's not available
*/
protected void reindex(ITask task, TaskData taskData) {
if (task == null) {
// this can happen when edits are discarded
return;
}
if (!taskIsIndexable(task, taskData)) {
return;
}
synchronized (reindexQueue) {
reindexQueue.put(task, taskData);
}
scheduleIndexMaintenance(MaintainIndexType.REINDEX);
}
private void addIndexedAttributes(Document document, ITask task, TaskAttribute root) {
addIndexedAttribute(document, FIELD_TASK_KEY, task.getTaskKey());
addIndexedAttribute(document, FIELD_REPOSITORY_URL, task.getRepositoryUrl());
addIndexedAttribute(document, FIELD_SUMMARY, root.getMappedAttribute(TaskAttribute.SUMMARY));
addIndexedAttribute(document, FIELD_CONTENT, ((AbstractTask) task).getNotes());
addIndexedAttribute(document, FIELD_NOTES, ((AbstractTask) task).getNotes());
for (TaskAttribute contentAttribute : computeContentAttributes(root)) {
addIndexedAttribute(document, FIELD_CONTENT, contentAttribute);
}
addIndexedDateAttributes(document, task);
TaskData taskData = root.getTaskData();
List<TaskAttribute> commentAttributes = taskData.getAttributeMapper().getAttributesByType(taskData,
TaskAttribute.TYPE_COMMENT);
for (TaskAttribute commentAttribute : commentAttributes) {
TaskComment taskComment = new TaskComment(taskData.getAttributeMapper().getTaskRepository(), task,
commentAttribute);
taskData.getAttributeMapper().updateTaskComment(taskComment, commentAttribute);
String text = taskComment.getText();
if (text.length() != 0) {
addIndexedAttribute(document, FIELD_CONTENT, text);
}
IRepositoryPerson author = taskComment.getAuthor();
if (author != null) {
addIndexedAttribute(document, FIELD_PERSON, author.getPersonId());
}
}
List<TaskAttribute> personAttributes = taskData.getAttributeMapper().getAttributesByType(taskData,
TaskAttribute.TYPE_PERSON);
for (TaskAttribute personAttribute : personAttributes) {
addIndexedAttribute(document, FIELD_PERSON, personAttribute);
}
TaskRepository repository = getRepositoryManager().getRepository(task.getConnectorKind(),
task.getRepositoryUrl());
if (repository != null) {
List<TaskAttribute> attachmentAttributes = taskData.getAttributeMapper().getAttributesByType(taskData,
TaskAttribute.TYPE_ATTACHMENT);
Set<String> attachmentNames = new HashSet<String>();
for (TaskAttribute attribute : attachmentAttributes) {
TaskAttachment taskAttachment = new TaskAttachment(repository, task, attribute);
taskData.getAttributeMapper().updateTaskAttachment(taskAttachment, attribute);
if (attachmentNames.add(taskAttachment.getFileName())) {
addIndexedAttribute(document, FIELD_ATTACHMENT_NAME, taskAttachment.getFileName());
}
addIndexedAttribute(document, FIELD_CONTENT, taskAttachment.getDescription());
}
}
for (AbstractTaskSchema.Field field : indexedFields) {
if (!specialFields.contains(field)) {
addIndexedAttribute(document, field, root.getMappedAttribute(field.getKey()));
}
}
}
/**
* compute attributes that should be indexed as {@link IndexField#CONTENT}
*/
private Collection<TaskAttribute> computeContentAttributes(TaskAttribute root) {
Set<TaskAttribute> attributes = new LinkedHashSet<TaskAttribute>();
// add default content attributes
{
TaskAttribute attribute = root.getMappedAttribute(TaskAttribute.SUMMARY);
if (attribute != null) {
attributes.add(attribute);
}
attribute = root.getMappedAttribute(TaskAttribute.DESCRIPTION);
if (attribute != null) {
attributes.add(attribute);
}
}
for (TaskAttribute attribute : root.getAttributes().values()) {
if (Boolean.parseBoolean(attribute.getMetaData().getValue(META_INDEXED_AS_CONTENT))) {
attributes.add(attribute);
}
}
return attributes;
}
private void addIndexedAttributes(Document document, ITask task) {
addIndexedAttribute(document, FIELD_TASK_KEY, task.getTaskKey());
addIndexedAttribute(document, FIELD_REPOSITORY_URL, task.getRepositoryUrl());
addIndexedAttribute(document, FIELD_SUMMARY, task.getSummary());
addIndexedAttribute(document, FIELD_CONTENT, task.getSummary());
addIndexedAttribute(document, FIELD_CONTENT, ((AbstractTask) task).getNotes());
addIndexedAttribute(document, FIELD_NOTES, ((AbstractTask) task).getNotes());
addIndexedDateAttributes(document, task);
}
private void addIndexedDateAttributes(Document document, ITask task) {
addIndexedAttribute(document, DefaultTaskSchema.getInstance().DATE_COMPLETION, task.getCompletionDate());
addIndexedAttribute(document, DefaultTaskSchema.getInstance().DATE_CREATION, task.getCreationDate());
addIndexedAttribute(document, DefaultTaskSchema.getInstance().DATE_DUE, task.getDueDate());
addIndexedAttribute(document, DefaultTaskSchema.getInstance().DATE_MODIFICATION, task.getModificationDate());
}
private void addIndexedAttribute(Document document, AbstractTaskSchema.Field indexField, TaskAttribute attribute) {
if (attribute == null) {
return;
}
List<String> values = attribute.getTaskData().getAttributeMapper().getValueLabels(attribute);
if (values.isEmpty()) {
return;
}
if (isPersonField(indexField)) {
IRepositoryPerson repositoryPerson = attribute.getTaskData()
.getAttributeMapper()
.getRepositoryPerson(attribute);
addIndexedAttribute(document, indexField, repositoryPerson);
if (values.size() <= 1) {
return;
}
}
for (String value : values) {
if (value.length() != 0) {
addIndexedAttribute(document, indexField, value);
}
}
}
private boolean isPersonField(org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field indexField) {
return TaskAttribute.TYPE_PERSON.equals(indexField.getType());
}
private void addIndexedAttribute(Document document, AbstractTaskSchema.Field indexField, IRepositoryPerson person) {
if (person != null) {
addIndexedAttribute(document, indexField, person.getPersonId());
addIndexedAttribute(document, indexField, person.getName());
}
}
private void addIndexedAttribute(Document document, AbstractTaskSchema.Field indexField, String value) {
if (value == null) {
return;
}
IndexableField field = document.getField(indexField.getIndexKey());
if (field == null) {
field = new TextField(indexField.getIndexKey(), value, Store.YES);
document.add(field);
} else {
String existingValue = field.stringValue();
if (!indexField.equals(FIELD_PERSON) || !existingValue.contains(value)) {
document.add(new TextField(field.name(), existingValue + " " + value, Store.YES)); //$NON-NLS-1$
}
}
}
private void addIndexedAttribute(Document document, AbstractTaskSchema.Field 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);
IndexableField field = document.getField(indexField.getIndexKey());
if (field == null) {
field = new StringField(indexField.getIndexKey(), value, Store.YES);
document.add(field);
} else {
document.add(new StringField(field.name(), value, Store.YES));
}
}
/**
* 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(AbstractTaskSchema.Field field, Date lowerBoundInclusive,
Date upperBoundInclusive) {
return field.getIndexKey() + ":[" + DateTools.dateToString(lowerBoundInclusive, Resolution.DAY) + " TO " //$NON-NLS-1$//$NON-NLS-2$
+ DateTools.dateToString(upperBoundInclusive, Resolution.DAY) + "]"; //$NON-NLS-1$
}
/**
* Indicates if the given task is indexable. The default implementation returns true, subclasses may override to
* filter some tasks from the task list. This method may be called more than once per task, with some calls omitting
* the task data. In this way implementations can avoid loading task data if the decision to filter tasks can be
* based on the ITask alone. Implementations that must read the task data in order to determine eligibility for
* indexing should return true for tasks where the provided task data is null.
*
* @param task
* the task
* @param taskData
* the task data, or null if there is no task data
* @return true if the given task should be indexed, otherwise false.
*/
protected boolean taskIsIndexable(ITask task, TaskData taskData) {
return true;
}
/**
* Escapes special characters in the given literal value so that they are not interpreted as special characters in a
* query.
*
* @param value
* the value to escape
* @return a representation of the value with characters escaped
*/
public String escapeFieldValue(String value) {
// see https://lucene.apache.org/core/6_1_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters
String escaped = value.replaceAll("([\\/\\+\\-\\!\\(\\)\\{\\}\\[\\]^\"~\\*\\?:\\\\]|&&|\\|\\|)", "\\\\$1"); //$NON-NLS-1$ //$NON-NLS-2$
return escaped;
}
private void maintainIndex(IProgressMonitor m) throws CoreException {
final int WORK_PER_SEGMENT = 1000;
SubMonitor monitor = SubMonitor.convert(m, 2 * WORK_PER_SEGMENT);
try {
try {
if (!rebuildIndex) {
try {
IndexReader reader = DirectoryReader.open(directory);
reader.close();
} catch (CorruptIndexException e) {
rebuildIndex = true;
}
}
if (rebuildIndex) {
synchronized (reindexQueue) {
reindexQueue.clear();
}
IStatus status = rebuildIndexCompletely(monitor.newChild(WORK_PER_SEGMENT));
if (!status.isOK()) {
StatusHandler.log(status);
}
} else {
monitor.worked(WORK_PER_SEGMENT);
}
// index any tasks that have been changed
indexQueuedTasks(monitor.newChild(WORK_PER_SEGMENT));
// prevent new searches from reading the now-stale index
closeIndexReader();
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
"Unexpected exception: " + e.getMessage(), e)); //$NON-NLS-1$
}
} finally {
monitor.done();
}
}
private void closeIndexReader() throws IOException {
Lock writeLock = indexReaderLock.writeLock();
writeLock.lock();
try {
synchronized (this) {
if (indexReader != null) {
indexReader.close();
indexReader = null;
}
}
} finally {
writeLock.unlock();
}
}
private void indexQueuedTasks(SubMonitor monitor)
throws CorruptIndexException, LockObtainFailedException, IOException, CoreException {
synchronized (reindexQueue) {
if (reindexQueue.isEmpty()) {
return;
}
monitor.beginTask(Messages.TaskListIndex_task_rebuilding_index, reindexQueue.size());
}
try {
IndexWriter writer = null;
try {
Map<ITask, TaskData> workingQueue = new HashMap<ITask, TaskData>();
// reindex tasks that are in the reindexQueue, making multiple passes so that we catch anything
// added/changed while we were reindexing
for (;;) {
workingQueue.clear();
synchronized (reindexQueue) {
if (reindexQueue.isEmpty()) {
break;
}
// move items from the reindexQueue to the temporary working queue
workingQueue.putAll(reindexQueue);
reindexQueue.keySet().removeAll(workingQueue.keySet());
}
if (writer == null) {
try {
writer = createIndexWriter(false);
} catch (CorruptIndexException e) {
rebuildIndex = true;
synchronized (reindexQueue) {
reindexQueue.clear();
}
rebuildIndexCompletely(monitor);
return;
}
}
monitor.setWorkRemaining(workingQueue.size());
for (Entry<ITask, TaskData> entry : workingQueue.entrySet()) {
ITask task = entry.getKey();
TaskData taskData = entry.getValue();
writer.deleteDocuments(new Term(FIELD_IDENTIFIER.getIndexKey(), task.getHandleIdentifier()));
add(writer, task, taskData);
monitor.worked(1);
}
}
} finally {
if (writer != null) {
writer.close();
}
}
} finally {
monitor.done();
}
}
private class TaskListState implements ITaskListRunnable {
List<ITask> indexableTasks;
public void execute(IProgressMonitor monitor) throws CoreException {
Collection<AbstractTask> tasks = taskList.getAllTasks();
indexableTasks = new ArrayList<ITask>(tasks.size());
for (ITask task : tasks) {
if (taskIsIndexable(task, null)) {
indexableTasks.add(task);
}
}
}
}
private IStatus rebuildIndexCompletely(SubMonitor monitor)
throws CorruptIndexException, LockObtainFailedException, IOException, CoreException {
MultiStatus multiStatus = new MultiStatus(TasksIndexCore.ID_PLUGIN, 0, null, null);
// get indexable tasks from the task list
final TaskListState taskListState = new TaskListState();
taskList.run(taskListState, monitor.newChild(0));
monitor.beginTask(Messages.TaskListIndex_task_rebuilding_index, taskListState.indexableTasks.size());
try {
IndexWriter writer;
try {
writer = createIndexWriter(true);
} catch (CorruptIndexException | IndexFormatTooOldException e) {
if (directory instanceof FSDirectory) {
cleanDirectory(((FSDirectory) directory).getDirectory().toFile());
writer = createIndexWriter(true);
} else {
throw e;
}
}
try {
for (ITask task : taskListState.indexableTasks) {
if (taskIsIndexable(task, null)) {
try {
TaskData taskData = dataManager.getTaskData(task);
add(writer, task, taskData);
} catch (CoreException e) {
// an individual task data error should not prevent the index from updating
multiStatus.add(e.getStatus());
}
}
monitor.worked(1);
}
synchronized (this) {
rebuildIndex = false;
}
} finally {
writer.close();
}
} finally {
monitor.done();
}
return multiStatus;
}
private void cleanDirectory(File file) throws IOException {
if (file.exists()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory()) {
cleanDirectory(child);
}
child.delete();
}
}
}
}
protected IndexWriter createIndexWriter(boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriterConfig writerConfig = new IndexWriterConfig(TaskAnalyzer.instance());
writerConfig.setInfoStream(InfoStream.NO_OUTPUT);
writerConfig.setOpenMode(create ? OpenMode.CREATE : OpenMode.APPEND);
return new IndexWriter(directory, writerConfig);
}
/**
* @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 {
if (!taskIsIndexable(task, taskData)) {
return;
}
Document document = new Document();
document.add(new TextField(FIELD_IDENTIFIER.getIndexKey(), task.getHandleIdentifier(), Store.YES));
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);
}
public void repositoryAdded(TaskRepository repository) {
// ignore
}
public void repositoryRemoved(TaskRepository repository) {
// ignore
}
public void repositorySettingsChanged(TaskRepository repository) {
// ignore
}
public void repositoryUrlChanged(TaskRepository repository, String oldUrl) {
reindex();
}
}