diff options
4 files changed, 2162 insertions, 2162 deletions
diff --git a/org.eclipse.mylyn.tasks.core/src/org/eclipse/mylyn/tasks/core/data/AbstractTaskSchema.java b/org.eclipse.mylyn.tasks.core/src/org/eclipse/mylyn/tasks/core/data/AbstractTaskSchema.java index 8b48c3b56..580e45ebb 100644 --- a/org.eclipse.mylyn.tasks.core/src/org/eclipse/mylyn/tasks/core/data/AbstractTaskSchema.java +++ b/org.eclipse.mylyn.tasks.core/src/org/eclipse/mylyn/tasks/core/data/AbstractTaskSchema.java @@ -1,285 +1,285 @@ -/*******************************************************************************
- * Copyright (c) 2011, 2012 Tasktop Technologies and others.
- * 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.tasks.core.data;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.eclipse.core.runtime.Assert;
-
-/**
- * Base class for task schemas. Clients should subclass to define a specific schema.
- *
- * @author Steffen Pingel
- * @author David Green
- * @since 3.5
- */
-public abstract class AbstractTaskSchema {
-
- public static class Field {
-
- private EnumSet<Flag> flags;
-
- private final String key;
-
- private final String label;
-
- private final String type;
-
- private final String indexKey;
-
- protected Field(String key, String label, String type) {
- this(key, label, type, (Flag[]) null);
- }
-
- protected Field(String key, String label, String type, Flag... flags) {
- this(key, label, type, null, flags);
- }
-
- /**
- * @param key
- * the task attribute key, which may be a common task attribute key defined in defined in
- * {@link TaskAttribute}
- * @param label
- * the user-visible label that is used by the user to identify this field
- * @param type
- * the type of the field, should be one of the constants defined in TaskAttribute (
- * <code>TaskAttribute.TYPE_*</code>)
- * @param indexKey
- * the index key, or null if this should not be indexed
- * @param flags
- * the flags, or null
- * @since 3.7
- */
- public Field(String key, String label, String type, String indexKey, Flag... flags) {
- this.indexKey = indexKey;
- Assert.isNotNull(key);
- Assert.isNotNull(label);
- Assert.isNotNull(type);
- this.key = key;
- this.label = label;
- this.type = type;
- if (flags == null || flags.length == 0) {
- this.flags = EnumSet.noneOf(Flag.class);
- } else {
- this.flags = EnumSet.copyOf(Arrays.asList(flags));
- }
- }
-
- public TaskAttribute createAttribute(TaskAttribute parent) {
- TaskAttribute attribute = parent.createMappedAttribute(getKey());
- // meta data
- TaskAttributeMetaData metaData = attribute.getMetaData();
- metaData.setLabel(getLabel());
- metaData.setType(getType());
- metaData.setReadOnly(isReadOnly());
- metaData.setKind(getKind());
- // options
- Map<String, String> options = getDefaultOptions();
- if (options != null) {
- for (Entry<String, String> option : options.entrySet()) {
- attribute.putOption(option.getKey(), option.getValue());
- }
- }
- return attribute;
- }
-
- public Map<String, String> getDefaultOptions() {
- return Collections.emptyMap();
- }
-
- public String getKey() {
- return key;
- }
-
- /**
- * the key to use when indexing this field
- *
- * @return the index key, or null if this should not be indexed
- * @since 3.7
- */
- public String getIndexKey() {
- return indexKey;
- }
-
- public String getKind() {
- if (flags.contains(Flag.ATTRIBUTE)) {
- return TaskAttribute.KIND_DEFAULT;
- } else if (flags.contains(Flag.PEOPLE)) {
- return TaskAttribute.KIND_PEOPLE;
- } else if (flags.contains(Flag.OPERATION)) {
- return TaskAttribute.KIND_OPERATION;
- }
- return null;
- }
-
- public String getLabel() {
- return label;
- }
-
- public String getType() {
- return type;
- }
-
- public boolean isReadOnly() {
- return flags.contains(Flag.READ_ONLY);
- }
-
- @Override
- public String toString() {
- return getLabel();
- }
-
- /**
- * @since 3.7
- */
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((indexKey == null) ? 0 : indexKey.hashCode());
- result = prime * result + ((key == null) ? 0 : key.hashCode());
- return result;
- }
-
- /**
- * @since 3.7
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- Field other = (Field) obj;
- if (indexKey == null) {
- if (other.indexKey != null) {
- return false;
- }
- } else if (!indexKey.equals(other.indexKey)) {
- return false;
- }
- if (key == null) {
- if (other.key != null) {
- return false;
- }
- } else if (!key.equals(other.key)) {
- return false;
- }
- return true;
- }
-
- }
-
- public enum Flag {
- ATTRIBUTE, OPERATION, PEOPLE, READ_ONLY
- };
-
- protected class FieldFactory {
-
- private EnumSet<Flag> flags;
-
- private String key;
-
- private String label;
-
- private String type;
-
- public FieldFactory(Field source) {
- this.flags = EnumSet.copyOf(source.flags);
- this.key = source.key;
- this.label = source.label;
- this.type = source.type;
- }
-
- public FieldFactory addFlags(Flag... flags) {
- this.flags.addAll(Arrays.asList(flags));
- return this;
- }
-
- public Field create() {
- return createField(key, label, type, (!flags.isEmpty()) ? flags.toArray(new Flag[0]) : null);
- }
-
- public FieldFactory flags(Flag... flags) {
- this.flags = EnumSet.copyOf(Arrays.asList(flags));
- return this;
- }
-
- public FieldFactory key(String key) {
- this.key = key;
- return this;
- }
-
- public FieldFactory label(String label) {
- this.label = label;
- return this;
- }
-
- public FieldFactory removeFlags(Flag... flags) {
- this.flags.removeAll(Arrays.asList(flags));
- return this;
- }
-
- public FieldFactory type(String type) {
- this.type = type;
- return this;
- }
-
- }
-
- private final Map<String, Field> fieldByKey = new LinkedHashMap<String, Field>();
-
- public Field getFieldByKey(String taskKey) {
- return fieldByKey.get(taskKey);
- }
-
- public void initialize(TaskData taskData) {
- for (Field field : fieldByKey.values()) {
- field.createAttribute(taskData.getRoot());
- }
- }
-
- protected Field createField(String key, String label, String type) {
- return createField(key, label, type, (Flag[]) null);
- }
-
- /**
- * @since 3.7
- * @see Field#Field(String, String, String, String, Flag...)
- */
- protected Field createField(String key, String label, String type, String indexKey, Flag... flags) {
- Field field = new Field(key, label, type, indexKey, flags);
- fieldByKey.put(key, field);
- return field;
- }
-
- protected Field createField(String key, String label, String type, Flag... flags) {
- Field field = new Field(key, label, type, flags);
- fieldByKey.put(key, field);
- return field;
- }
-
- protected FieldFactory inheritFrom(Field source) {
- return new FieldFactory(source);
- }
-
-}
+/******************************************************************************* + * Copyright (c) 2011, 2012 Tasktop Technologies and others. + * 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.tasks.core.data; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.core.runtime.Assert; + +/** + * Base class for task schemas. Clients should subclass to define a specific schema. + * + * @author Steffen Pingel + * @author David Green + * @since 3.5 + */ +public abstract class AbstractTaskSchema { + + public static class Field { + + private EnumSet<Flag> flags; + + private final String key; + + private final String label; + + private final String type; + + private final String indexKey; + + protected Field(String key, String label, String type) { + this(key, label, type, (Flag[]) null); + } + + protected Field(String key, String label, String type, Flag... flags) { + this(key, label, type, null, flags); + } + + /** + * @param key + * the task attribute key, which may be a common task attribute key defined in defined in + * {@link TaskAttribute} + * @param label + * the user-visible label that is used by the user to identify this field + * @param type + * the type of the field, should be one of the constants defined in TaskAttribute ( + * <code>TaskAttribute.TYPE_*</code>) + * @param indexKey + * the index key, or null if this should not be indexed + * @param flags + * the flags, or null + * @since 3.7 + */ + public Field(String key, String label, String type, String indexKey, Flag... flags) { + this.indexKey = indexKey; + Assert.isNotNull(key); + Assert.isNotNull(label); + Assert.isNotNull(type); + this.key = key; + this.label = label; + this.type = type; + if (flags == null || flags.length == 0) { + this.flags = EnumSet.noneOf(Flag.class); + } else { + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + } + } + + public TaskAttribute createAttribute(TaskAttribute parent) { + TaskAttribute attribute = parent.createMappedAttribute(getKey()); + // meta data + TaskAttributeMetaData metaData = attribute.getMetaData(); + metaData.setLabel(getLabel()); + metaData.setType(getType()); + metaData.setReadOnly(isReadOnly()); + metaData.setKind(getKind()); + // options + Map<String, String> options = getDefaultOptions(); + if (options != null) { + for (Entry<String, String> option : options.entrySet()) { + attribute.putOption(option.getKey(), option.getValue()); + } + } + return attribute; + } + + public Map<String, String> getDefaultOptions() { + return Collections.emptyMap(); + } + + public String getKey() { + return key; + } + + /** + * the key to use when indexing this field + * + * @return the index key, or null if this should not be indexed + * @since 3.7 + */ + public String getIndexKey() { + return indexKey; + } + + public String getKind() { + if (flags.contains(Flag.ATTRIBUTE)) { + return TaskAttribute.KIND_DEFAULT; + } else if (flags.contains(Flag.PEOPLE)) { + return TaskAttribute.KIND_PEOPLE; + } else if (flags.contains(Flag.OPERATION)) { + return TaskAttribute.KIND_OPERATION; + } + return null; + } + + public String getLabel() { + return label; + } + + public String getType() { + return type; + } + + public boolean isReadOnly() { + return flags.contains(Flag.READ_ONLY); + } + + @Override + public String toString() { + return getLabel(); + } + + /** + * @since 3.7 + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((indexKey == null) ? 0 : indexKey.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + /** + * @since 3.7 + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Field other = (Field) obj; + if (indexKey == null) { + if (other.indexKey != null) { + return false; + } + } else if (!indexKey.equals(other.indexKey)) { + return false; + } + if (key == null) { + if (other.key != null) { + return false; + } + } else if (!key.equals(other.key)) { + return false; + } + return true; + } + + } + + public enum Flag { + ATTRIBUTE, OPERATION, PEOPLE, READ_ONLY + }; + + protected class FieldFactory { + + private EnumSet<Flag> flags; + + private String key; + + private String label; + + private String type; + + public FieldFactory(Field source) { + this.flags = EnumSet.copyOf(source.flags); + this.key = source.key; + this.label = source.label; + this.type = source.type; + } + + public FieldFactory addFlags(Flag... flags) { + this.flags.addAll(Arrays.asList(flags)); + return this; + } + + public Field create() { + return createField(key, label, type, (!flags.isEmpty()) ? flags.toArray(new Flag[0]) : null); + } + + public FieldFactory flags(Flag... flags) { + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + return this; + } + + public FieldFactory key(String key) { + this.key = key; + return this; + } + + public FieldFactory label(String label) { + this.label = label; + return this; + } + + public FieldFactory removeFlags(Flag... flags) { + this.flags.removeAll(Arrays.asList(flags)); + return this; + } + + public FieldFactory type(String type) { + this.type = type; + return this; + } + + } + + private final Map<String, Field> fieldByKey = new LinkedHashMap<String, Field>(); + + public Field getFieldByKey(String taskKey) { + return fieldByKey.get(taskKey); + } + + public void initialize(TaskData taskData) { + for (Field field : fieldByKey.values()) { + field.createAttribute(taskData.getRoot()); + } + } + + protected Field createField(String key, String label, String type) { + return createField(key, label, type, (Flag[]) null); + } + + /** + * @since 3.7 + * @see Field#Field(String, String, String, String, Flag...) + */ + protected Field createField(String key, String label, String type, String indexKey, Flag... flags) { + Field field = new Field(key, label, type, indexKey, flags); + fieldByKey.put(key, field); + return field; + } + + protected Field createField(String key, String label, String type, Flag... flags) { + Field field = new Field(key, label, type, flags); + fieldByKey.put(key, field); + return field; + } + + protected FieldFactory inheritFrom(Field source) { + return new FieldFactory(source); + } + +} diff --git a/org.eclipse.mylyn.tasks.index.core/src/org/eclipse/mylyn/internal/tasks/index/core/TaskListIndex.java b/org.eclipse.mylyn.tasks.index.core/src/org/eclipse/mylyn/internal/tasks/index/core/TaskListIndex.java index a121439b0..1d6b5af36 100644 --- a/org.eclipse.mylyn.tasks.index.core/src/org/eclipse/mylyn/internal/tasks/index/core/TaskListIndex.java +++ b/org.eclipse.mylyn.tasks.index.core/src/org/eclipse/mylyn/internal/tasks/index/core/TaskListIndex.java @@ -1,1200 +1,1200 @@ -/*******************************************************************************
- * Copyright (c) 2011, 2012 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 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;
-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.LockObtainFailedException;
-import org.apache.lucene.store.NIOFSDirectory;
-import org.apache.lucene.util.Version;
-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.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
- */
-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$
-
- 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;
-
- 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) {
- 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", e); //$NON-NLS-1$
- logStatus.add(e.getStatus());
- StatusHandler.log(logStatus);
- }
- return Status.OK_STATUS;
- }
-
- }
-
- public abstract static class TaskCollector {
-
- public abstract void collect(ITask task);
-
- }
-
- 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);
-
- addIndexedField(FIELD_IDENTIFIER);
- addIndexedField(FIELD_TASK_KEY);
- addIndexedField(FIELD_REPOSITORY_URL);
- addIndexedField(FIELD_SUMMARY);
- addIndexedField(FIELD_CONTENT);
- 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);
- }
-
- private static enum MaintainIndexType {
- STARTUP, REINDEX
- }
-
- // FIXME: document concurrency model
-
- 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;
- if (!indexLocation.exists()) {
- rebuildIndex = true;
- if (!indexLocation.mkdirs()) {
- StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
- "Cannot create task list index folder: " + indexLocation)); //$NON-NLS-1$
- }
- }
- if (indexLocation.exists() && indexLocation.isDirectory()) {
- try {
- directory = new NIOFSDirectory(indexLocation);
- } catch (IOException e) {
- StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN,
- "Cannot create task list index", e)); //$NON-NLS-1$
- }
- }
- 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;
- }
-
- /**
- * 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", e); //$NON-NLS-1$
- 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$
- } finally {
- try {
- indexSearcher.close();
- } catch (IOException e) {
- // ignore
- }
- }
-
- } 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 {
- try {
- indexSearcher.close();
- } catch (IOException e) {
- // ignore
- }
- }
- }
- } 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 (patternString.indexOf(':') == -1 && !hasBooleanSpecifiers && defaultField.equals(FIELD_SUMMARY)
- && patternString.indexOf('"') == -1) {
- return new PrefixQuery(new Term(defaultField.getIndexKey(), patternString));
- }
- QueryParser qp = new QueryParser(Version.LUCENE_CURRENT, defaultField.getIndexKey(), new TaskAnalyzer());
- 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) {
- 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);
- 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;
- }
- }
- 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 = IndexReader.open(directory, true);
- 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));
-
- for (TaskAttribute contentAttribute : computeContentAttributes(root)) {
- addIndexedAttribute(document, FIELD_CONTENT, contentAttribute);
- }
-
- addIndexedDateAttributes(document, task);
-
- List<TaskAttribute> commentAttributes = root.getTaskData()
- .getAttributeMapper()
- .getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_COMMENT);
- for (TaskAttribute commentAttribute : commentAttributes) {
-
- TaskComment taskComment = new TaskComment(root.getTaskData().getAttributeMapper().getTaskRepository(),
- task, commentAttribute);
- root.getTaskData().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 = root.getTaskData()
- .getAttributeMapper()
- .getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_PERSON);
- for (TaskAttribute personAttribute : personAttributes) {
- addIndexedAttribute(document, FIELD_PERSON, personAttribute);
- }
-
- 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());
- 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;
- }
- Field field = document.getField(indexField.getIndexKey());
- if (field == null) {
- field = new Field(indexField.getIndexKey(), value, Store.YES,
- org.apache.lucene.document.Field.Index.ANALYZED);
- document.add(field);
- } else {
- String existingValue = field.stringValue();
- if (!indexField.equals(FIELD_PERSON) || !existingValue.contains(value)) {
- field.setValue(existingValue + " " + value); //$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);
- Field field = document.getField(indexField.getIndexKey());
- if (field == null) {
- field = new Field(indexField.getIndexKey(), value, Store.YES,
- org.apache.lucene.document.Field.Index.ANALYZED);
- document.add(field);
- } else {
- field.setValue(value);
- }
- }
-
- /**
- * 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 " + DateTools.dateToString(upperBoundInclusive, Resolution.DAY) + "]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
- }
-
- /**
- * 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 http://lucene.apache.org/java/2_9_1/queryparsersyntax.html#Escaping%20Special%20Characters
- 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 = IndexReader.open(directory, false);
- 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 {
-
- 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) {
- writer = new IndexWriter(directory, new TaskAnalyzer(), false,
- IndexWriter.MaxFieldLength.UNLIMITED);
- }
-
- 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 {
- final IndexWriter writer = new IndexWriter(directory, new TaskAnalyzer(), true,
- IndexWriter.MaxFieldLength.UNLIMITED);
- 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;
- }
-
- /**
- * @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 Field(FIELD_IDENTIFIER.getIndexKey(), 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);
- }
-
- 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();
- }
-}
+/******************************************************************************* + * Copyright (c) 2011, 2012 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 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; +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.LockObtainFailedException; +import org.apache.lucene.store.NIOFSDirectory; +import org.apache.lucene.util.Version; +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.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 + */ +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$ + + 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; + + 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) { + 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", e); //$NON-NLS-1$ + logStatus.add(e.getStatus()); + StatusHandler.log(logStatus); + } + return Status.OK_STATUS; + } + + } + + public abstract static class TaskCollector { + + public abstract void collect(ITask task); + + } + + 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); + + addIndexedField(FIELD_IDENTIFIER); + addIndexedField(FIELD_TASK_KEY); + addIndexedField(FIELD_REPOSITORY_URL); + addIndexedField(FIELD_SUMMARY); + addIndexedField(FIELD_CONTENT); + 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); + } + + private static enum MaintainIndexType { + STARTUP, REINDEX + } + + // FIXME: document concurrency model + + 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; + if (!indexLocation.exists()) { + rebuildIndex = true; + if (!indexLocation.mkdirs()) { + StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN, + "Cannot create task list index folder: " + indexLocation)); //$NON-NLS-1$ + } + } + if (indexLocation.exists() && indexLocation.isDirectory()) { + try { + directory = new NIOFSDirectory(indexLocation); + } catch (IOException e) { + StatusHandler.log(new Status(IStatus.ERROR, TasksIndexCore.ID_PLUGIN, + "Cannot create task list index", e)); //$NON-NLS-1$ + } + } + 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; + } + + /** + * 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", e); //$NON-NLS-1$ + 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$ + } finally { + try { + indexSearcher.close(); + } catch (IOException e) { + // ignore + } + } + + } 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 { + try { + indexSearcher.close(); + } catch (IOException e) { + // ignore + } + } + } + } 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 (patternString.indexOf(':') == -1 && !hasBooleanSpecifiers && defaultField.equals(FIELD_SUMMARY) + && patternString.indexOf('"') == -1) { + return new PrefixQuery(new Term(defaultField.getIndexKey(), patternString)); + } + QueryParser qp = new QueryParser(Version.LUCENE_CURRENT, defaultField.getIndexKey(), new TaskAnalyzer()); + 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) { + 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); + 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; + } + } + 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 = IndexReader.open(directory, true); + 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)); + + for (TaskAttribute contentAttribute : computeContentAttributes(root)) { + addIndexedAttribute(document, FIELD_CONTENT, contentAttribute); + } + + addIndexedDateAttributes(document, task); + + List<TaskAttribute> commentAttributes = root.getTaskData() + .getAttributeMapper() + .getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_COMMENT); + for (TaskAttribute commentAttribute : commentAttributes) { + + TaskComment taskComment = new TaskComment(root.getTaskData().getAttributeMapper().getTaskRepository(), + task, commentAttribute); + root.getTaskData().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 = root.getTaskData() + .getAttributeMapper() + .getAttributesByType(root.getTaskData(), TaskAttribute.TYPE_PERSON); + for (TaskAttribute personAttribute : personAttributes) { + addIndexedAttribute(document, FIELD_PERSON, personAttribute); + } + + 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()); + 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; + } + Field field = document.getField(indexField.getIndexKey()); + if (field == null) { + field = new Field(indexField.getIndexKey(), value, Store.YES, + org.apache.lucene.document.Field.Index.ANALYZED); + document.add(field); + } else { + String existingValue = field.stringValue(); + if (!indexField.equals(FIELD_PERSON) || !existingValue.contains(value)) { + field.setValue(existingValue + " " + value); //$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); + Field field = document.getField(indexField.getIndexKey()); + if (field == null) { + field = new Field(indexField.getIndexKey(), value, Store.YES, + org.apache.lucene.document.Field.Index.ANALYZED); + document.add(field); + } else { + field.setValue(value); + } + } + + /** + * 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 " + DateTools.dateToString(upperBoundInclusive, Resolution.DAY) + "]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ + } + + /** + * 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 http://lucene.apache.org/java/2_9_1/queryparsersyntax.html#Escaping%20Special%20Characters + 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 = IndexReader.open(directory, false); + 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 { + + 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) { + writer = new IndexWriter(directory, new TaskAnalyzer(), false, + IndexWriter.MaxFieldLength.UNLIMITED); + } + + 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 { + final IndexWriter writer = new IndexWriter(directory, new TaskAnalyzer(), true, + IndexWriter.MaxFieldLength.UNLIMITED); + 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; + } + + /** + * @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 Field(FIELD_IDENTIFIER.getIndexKey(), 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); + } + + 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(); + } +} diff --git a/org.eclipse.mylyn.tasks.index.tests/src/org/eclipse/mylyn/internal/tasks/index/tests/TaskListIndexTest.java b/org.eclipse.mylyn.tasks.index.tests/src/org/eclipse/mylyn/internal/tasks/index/tests/TaskListIndexTest.java index 76aa27294..34a71db1c 100644 --- a/org.eclipse.mylyn.tasks.index.tests/src/org/eclipse/mylyn/internal/tasks/index/tests/TaskListIndexTest.java +++ b/org.eclipse.mylyn.tasks.index.tests/src/org/eclipse/mylyn/internal/tasks/index/tests/TaskListIndexTest.java @@ -1,415 +1,415 @@ -/*******************************************************************************
- * Copyright (c) 2011, 2012 Tasktop Technologies and others.
- * 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.tests;
-
-import static junit.framework.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.logging.Logger;
-
-import junit.framework.Assert;
-
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.mylyn.commons.core.DelegatingProgressMonitor;
-import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
-import org.eclipse.mylyn.internal.tasks.core.LocalTask;
-import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex;
-import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex.TaskCollector;
-import org.eclipse.mylyn.internal.tasks.index.tests.util.MockTestContext;
-import org.eclipse.mylyn.tasks.core.IRepositoryManager;
-import org.eclipse.mylyn.tasks.core.ITask;
-import org.eclipse.mylyn.tasks.core.data.DefaultTaskSchema;
-import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
-import org.eclipse.mylyn.tasks.core.data.TaskData;
-import org.eclipse.mylyn.tasks.core.data.TaskMapper;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * @author David Green
- */
-public class TaskListIndexTest {
-
- private static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_SUMMARY = DefaultTaskSchema.getInstance().SUMMARY;
-
- private static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_DATE_CREATION = DefaultTaskSchema.getInstance().DATE_CREATION;
-
- private static class TestTaskCollector extends TaskCollector {
-
- private final List<ITask> tasks = new ArrayList<ITask>();
-
- @Override
- public void collect(ITask task) {
- tasks.add(task);
- }
-
- public List<ITask> getTasks() {
- return tasks;
- }
- }
-
- private MockTestContext context;
-
- private TaskListIndex index;
-
- private File tempDir;
-
- @Before
- public void setup() throws IOException {
- tempDir = File.createTempFile(TaskListIndexTest.class.getSimpleName(), ".tmp");
- tempDir.delete();
- tempDir.mkdirs();
-
- assertTrue(tempDir.exists() && tempDir.isDirectory());
-
- context = new MockTestContext();
- }
-
- @After
- public void tearDown() {
- if (index != null) {
- try {
- index.waitUntilIdle();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- index.close();
- index = null;
- }
- if (tempDir != null) {
- delete(tempDir);
- assertFalse(tempDir.exists());
- }
- }
-
- private void delete(File file) {
- if (file.isDirectory()) {
- File[] children = file.listFiles();
- if (children != null) {
- for (File child : children) {
- delete(child);
- }
- }
- }
- if (!file.delete()) {
- Logger.getLogger(TaskListIndexTest.class.getName()).severe("Cannot delete: " + file);
- }
- }
-
- private void setupIndex() {
- index = new TaskListIndex(context.getTaskList(), context.getDataManager(),
- (IRepositoryManager) context.getRepositoryManager(), tempDir, 0L);
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
- index.setReindexDelay(0L);
- }
-
- @Test
- public void testMatchesLocalTaskOnSummary() throws InterruptedException {
- setupIndex();
-
- ITask task = context.createLocalTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- assertTrue(index.matches(task, task.getSummary()));
- assertFalse(index.matches(task, "" + System.currentTimeMillis()));
-
- index.setDefaultField(FIELD_SUMMARY);
-
- assertTrue(index.matches(task, task.getSummary()));
- assertFalse(index.matches(task, "" + System.currentTimeMillis()));
- }
-
- @Test
- public void testMatchesLocalTaskOnDescription() throws InterruptedException {
- setupIndex();
-
- ITask task = context.createLocalTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- assertTrue(index.matches(task, ((LocalTask) task).getNotes()));
- assertFalse(index.matches(task, "unlikely-akjfsaow"));
-
- index.setDefaultField(FIELD_SUMMARY);
-
- assertFalse(index.matches(task, ((LocalTask) task).getNotes()));
- }
-
- @Test
- public void testMatchesRepositoryTaskOnSummary() throws InterruptedException, CoreException {
- setupIndex();
-
- ITask task = context.createRepositoryTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- assertTrue(index.matches(task, task.getSummary()));
- assertFalse(index.matches(task, "" + System.currentTimeMillis()));
-
- index.setDefaultField(FIELD_SUMMARY);
-
- assertTrue(index.matches(task, task.getSummary()));
- assertFalse(index.matches(task, "" + System.currentTimeMillis()));
- }
-
- @Test
- public void testMatchesRepositoryTaskOnDescription() throws InterruptedException, CoreException {
- setupIndex();
-
- ITask task = context.createRepositoryTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- TaskData taskData = context.getDataManager().getTaskData(task);
- assertNotNull(taskData);
-
- TaskMapper taskMapping = context.getMockRepositoryConnector().getTaskMapping(taskData);
-
- assertTrue(index.matches(task, taskMapping.getDescription()));
- assertFalse(index.matches(task, "unlikely-akjfsaow"));
-
- index.setDefaultField(FIELD_SUMMARY);
-
- assertFalse(index.matches(task, taskMapping.getDescription()));
- }
-
- @Test
- public void testFind() throws InterruptedException {
- setupIndex();
-
- ITask task = context.createLocalTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(FIELD_SUMMARY);
-
- TestTaskCollector collector = new TestTaskCollector();
- index.find(task.getSummary(), collector, 1000);
-
- assertEquals(1, collector.getTasks().size());
- assertTrue(collector.getTasks().contains(task));
- }
-
- @Test
- public void testMatchesRepositoryTaskOnCreationDate() throws InterruptedException, CoreException {
- setupIndex();
-
- ITask task = context.createRepositoryTask();
-
- Date creationDate = task.getCreationDate();
- assertNotNull(creationDate);
-
- index.waitUntilIdle();
-
- assertFalse(index.matches(task, FIELD_DATE_CREATION.getIndexKey() + ":[20010101 TO 20010105]"));
-
- String matchDate = new SimpleDateFormat("yyyyMMdd").format(creationDate);
- matchDate = Integer.toString(Integer.parseInt(matchDate) + 2);
-
- String patternString = FIELD_DATE_CREATION.getIndexKey() + ":[20111019 TO " + matchDate + "]";
-
- System.out.println(patternString);
-
- assertTrue(index.matches(task, patternString));
- }
-
- @Test
- public void testMatchesOnRepositoryUrl() throws Exception {
- setupIndex();
-
- ITask repositoryTask = context.createRepositoryTask();
- ITask localTask = context.createLocalTask();
-
- index.waitUntilIdle();
-
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- TaskData taskData = context.getDataManager().getTaskData(repositoryTask);
-
- // sanity
- assertNotNull(taskData);
- assertNotNull(taskData.getRepositoryUrl());
- assertFalse(taskData.getRepositoryUrl().length() == 0);
-
- // setup descriptions so that they will both match
- final String content = "RepositoryUrl";
- taskData.getRoot().getMappedAttribute(TaskAttribute.DESCRIPTION).setValue(content);
- ((AbstractTask) localTask).setNotes(content);
-
- context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor());
-
- Set<ITask> changedElements = new HashSet<ITask>();
- changedElements.add(localTask);
- changedElements.add(repositoryTask);
- context.getTaskList().notifyElementsChanged(changedElements);
-
- index.waitUntilIdle();
-
- assertTrue(index.matches(localTask, content));
- assertTrue(index.matches(repositoryTask, content));
-
- String repositoryUrlQuery = content + " AND " + TaskListIndex.FIELD_REPOSITORY_URL.getIndexKey() + ":\""
- + index.escapeFieldValue(repositoryTask.getRepositoryUrl()) + "\"";
- assertFalse(index.matches(localTask, repositoryUrlQuery));
- assertTrue(index.matches(repositoryTask, repositoryUrlQuery));
- }
-
- @Test
- public void testCharacterEscaping() {
- setupIndex();
- for (String special : new String[] { "+", "-", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~",
- "*", "?", ":", "\\" }) {
- assertEquals("a\\" + special + "b", index.escapeFieldValue("a" + special + "b"));
- }
- }
-
- @Test
- public void testAttributeMetadataAffectsIndexing() throws CoreException, InterruptedException {
- setupIndex();
-
- ITask repositoryTask = context.createRepositoryTask();
-
- index.waitUntilIdle();
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- TaskData taskData = context.getDataManager().getTaskData(repositoryTask);
-
- // sanity
- assertNotNull(taskData);
-
- final String content = "c" + System.currentTimeMillis();
-
- // setup data so that it will match
- TaskAttribute attribute = taskData.getRoot().createAttribute("unusualIndexedAttribute");
- attribute.setValue(content);
-
- // update
- context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor());
-
- // verify index doesn't match search term
- assertFalse(index.matches(repositoryTask, content));
-
- // now make data indexable
- attribute.getMetaData().putValue(TaskAttribute.META_INDEXED_AS_CONTENT, "true");
- // update
- context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor());
-
- // should now match
- assertTrue(index.matches(repositoryTask, content));
- }
-
- /**
- * Verify that multiple threads can concurrently use the index to find tasks, i.e. that no threads are blocked from
- * finding tasks by other threads.
- */
- @Test
- public void testMultithreadedAccessOnFind() throws CoreException, InterruptedException, ExecutionException {
- setupIndex();
-
- final ITask repositoryTask = context.createRepositoryTask();
-
- index.waitUntilIdle();
- index.setDefaultField(TaskListIndex.FIELD_CONTENT);
-
- final int nThreads = 10;
- final int[] concurrencyLevel = new int[1];
- ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
- try {
- Collection<Callable<Object>> tasks = new HashSet<Callable<Object>>();
- for (int x = 0; x < nThreads; ++x) {
- tasks.add(new Callable<Object>() {
-
- public Object call() throws Exception {
- final int[] hitCount = new int[1];
- index.find(repositoryTask.getSummary(), new TaskCollector() {
-
- @Override
- public void collect(ITask task) {
- synchronized (concurrencyLevel) {
- ++concurrencyLevel[0];
- if (concurrencyLevel[0] < nThreads) {
- try {
- concurrencyLevel.wait(5000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- } else {
- concurrencyLevel.notifyAll();
- }
- }
- ++hitCount[0];
- }
- }, 100);
- return hitCount[0] == 1;
- }
- });
- }
- List<Future<Object>> futures = executorService.invokeAll(tasks);
- for (Future<Object> future : futures) {
- assertEquals(Boolean.TRUE, future.get());
- }
- Assert.assertEquals(nThreads, concurrencyLevel[0]);
- } finally {
- executorService.shutdownNow();
- }
- }
-
- @Test
- public void testRepositoryUrlChanged() throws InterruptedException, CoreException {
- setupIndex();
-
- ITask repositoryTask = context.createRepositoryTask();
- final String originalHandle = repositoryTask.getHandleIdentifier();
-
- index.waitUntilIdle();
-
- final String newUrl = context.getMockRepository().getRepositoryUrl() + "/changed";
-
- context.refactorMockRepositoryUrl(newUrl);
-
- Assert.assertFalse(originalHandle.equals(repositoryTask.getHandleIdentifier()));
-
- index.waitUntilIdle();
-
- Assert.assertTrue(index.matches(
- repositoryTask,
- TaskListIndex.FIELD_IDENTIFIER.getIndexKey() + ":"
- + index.escapeFieldValue(repositoryTask.getHandleIdentifier())));
- }
-
+/******************************************************************************* + * Copyright (c) 2011, 2012 Tasktop Technologies and others. + * 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.tests; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +import junit.framework.Assert; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.mylyn.commons.core.DelegatingProgressMonitor; +import org.eclipse.mylyn.internal.tasks.core.AbstractTask; +import org.eclipse.mylyn.internal.tasks.core.LocalTask; +import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex; +import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex.TaskCollector; +import org.eclipse.mylyn.internal.tasks.index.tests.util.MockTestContext; +import org.eclipse.mylyn.tasks.core.IRepositoryManager; +import org.eclipse.mylyn.tasks.core.ITask; +import org.eclipse.mylyn.tasks.core.data.DefaultTaskSchema; +import org.eclipse.mylyn.tasks.core.data.TaskAttribute; +import org.eclipse.mylyn.tasks.core.data.TaskData; +import org.eclipse.mylyn.tasks.core.data.TaskMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * @author David Green + */ +public class TaskListIndexTest { + + private static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_SUMMARY = DefaultTaskSchema.getInstance().SUMMARY; + + private static final org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field FIELD_DATE_CREATION = DefaultTaskSchema.getInstance().DATE_CREATION; + + private static class TestTaskCollector extends TaskCollector { + + private final List<ITask> tasks = new ArrayList<ITask>(); + + @Override + public void collect(ITask task) { + tasks.add(task); + } + + public List<ITask> getTasks() { + return tasks; + } + } + + private MockTestContext context; + + private TaskListIndex index; + + private File tempDir; + + @Before + public void setup() throws IOException { + tempDir = File.createTempFile(TaskListIndexTest.class.getSimpleName(), ".tmp"); + tempDir.delete(); + tempDir.mkdirs(); + + assertTrue(tempDir.exists() && tempDir.isDirectory()); + + context = new MockTestContext(); + } + + @After + public void tearDown() { + if (index != null) { + try { + index.waitUntilIdle(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + index.close(); + index = null; + } + if (tempDir != null) { + delete(tempDir); + assertFalse(tempDir.exists()); + } + } + + private void delete(File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + delete(child); + } + } + } + if (!file.delete()) { + Logger.getLogger(TaskListIndexTest.class.getName()).severe("Cannot delete: " + file); + } + } + + private void setupIndex() { + index = new TaskListIndex(context.getTaskList(), context.getDataManager(), + (IRepositoryManager) context.getRepositoryManager(), tempDir, 0L); + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + index.setReindexDelay(0L); + } + + @Test + public void testMatchesLocalTaskOnSummary() throws InterruptedException { + setupIndex(); + + ITask task = context.createLocalTask(); + + index.waitUntilIdle(); + + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + assertTrue(index.matches(task, task.getSummary())); + assertFalse(index.matches(task, "" + System.currentTimeMillis())); + + index.setDefaultField(FIELD_SUMMARY); + + assertTrue(index.matches(task, task.getSummary())); + assertFalse(index.matches(task, "" + System.currentTimeMillis())); + } + + @Test + public void testMatchesLocalTaskOnDescription() throws InterruptedException { + setupIndex(); + + ITask task = context.createLocalTask(); + + index.waitUntilIdle(); + + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + assertTrue(index.matches(task, ((LocalTask) task).getNotes())); + assertFalse(index.matches(task, "unlikely-akjfsaow")); + + index.setDefaultField(FIELD_SUMMARY); + + assertFalse(index.matches(task, ((LocalTask) task).getNotes())); + } + + @Test + public void testMatchesRepositoryTaskOnSummary() throws InterruptedException, CoreException { + setupIndex(); + + ITask task = context.createRepositoryTask(); + + index.waitUntilIdle(); + + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + assertTrue(index.matches(task, task.getSummary())); + assertFalse(index.matches(task, "" + System.currentTimeMillis())); + + index.setDefaultField(FIELD_SUMMARY); + + assertTrue(index.matches(task, task.getSummary())); + assertFalse(index.matches(task, "" + System.currentTimeMillis())); + } + + @Test + public void testMatchesRepositoryTaskOnDescription() throws InterruptedException, CoreException { + setupIndex(); + + ITask task = context.createRepositoryTask(); + + index.waitUntilIdle(); + + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + TaskData taskData = context.getDataManager().getTaskData(task); + assertNotNull(taskData); + + TaskMapper taskMapping = context.getMockRepositoryConnector().getTaskMapping(taskData); + + assertTrue(index.matches(task, taskMapping.getDescription())); + assertFalse(index.matches(task, "unlikely-akjfsaow")); + + index.setDefaultField(FIELD_SUMMARY); + + assertFalse(index.matches(task, taskMapping.getDescription())); + } + + @Test + public void testFind() throws InterruptedException { + setupIndex(); + + ITask task = context.createLocalTask(); + + index.waitUntilIdle(); + + index.setDefaultField(FIELD_SUMMARY); + + TestTaskCollector collector = new TestTaskCollector(); + index.find(task.getSummary(), collector, 1000); + + assertEquals(1, collector.getTasks().size()); + assertTrue(collector.getTasks().contains(task)); + } + + @Test + public void testMatchesRepositoryTaskOnCreationDate() throws InterruptedException, CoreException { + setupIndex(); + + ITask task = context.createRepositoryTask(); + + Date creationDate = task.getCreationDate(); + assertNotNull(creationDate); + + index.waitUntilIdle(); + + assertFalse(index.matches(task, FIELD_DATE_CREATION.getIndexKey() + ":[20010101 TO 20010105]")); + + String matchDate = new SimpleDateFormat("yyyyMMdd").format(creationDate); + matchDate = Integer.toString(Integer.parseInt(matchDate) + 2); + + String patternString = FIELD_DATE_CREATION.getIndexKey() + ":[20111019 TO " + matchDate + "]"; + + System.out.println(patternString); + + assertTrue(index.matches(task, patternString)); + } + + @Test + public void testMatchesOnRepositoryUrl() throws Exception { + setupIndex(); + + ITask repositoryTask = context.createRepositoryTask(); + ITask localTask = context.createLocalTask(); + + index.waitUntilIdle(); + + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + TaskData taskData = context.getDataManager().getTaskData(repositoryTask); + + // sanity + assertNotNull(taskData); + assertNotNull(taskData.getRepositoryUrl()); + assertFalse(taskData.getRepositoryUrl().length() == 0); + + // setup descriptions so that they will both match + final String content = "RepositoryUrl"; + taskData.getRoot().getMappedAttribute(TaskAttribute.DESCRIPTION).setValue(content); + ((AbstractTask) localTask).setNotes(content); + + context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor()); + + Set<ITask> changedElements = new HashSet<ITask>(); + changedElements.add(localTask); + changedElements.add(repositoryTask); + context.getTaskList().notifyElementsChanged(changedElements); + + index.waitUntilIdle(); + + assertTrue(index.matches(localTask, content)); + assertTrue(index.matches(repositoryTask, content)); + + String repositoryUrlQuery = content + " AND " + TaskListIndex.FIELD_REPOSITORY_URL.getIndexKey() + ":\"" + + index.escapeFieldValue(repositoryTask.getRepositoryUrl()) + "\""; + assertFalse(index.matches(localTask, repositoryUrlQuery)); + assertTrue(index.matches(repositoryTask, repositoryUrlQuery)); + } + + @Test + public void testCharacterEscaping() { + setupIndex(); + for (String special : new String[] { "+", "-", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", + "*", "?", ":", "\\" }) { + assertEquals("a\\" + special + "b", index.escapeFieldValue("a" + special + "b")); + } + } + + @Test + public void testAttributeMetadataAffectsIndexing() throws CoreException, InterruptedException { + setupIndex(); + + ITask repositoryTask = context.createRepositoryTask(); + + index.waitUntilIdle(); + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + TaskData taskData = context.getDataManager().getTaskData(repositoryTask); + + // sanity + assertNotNull(taskData); + + final String content = "c" + System.currentTimeMillis(); + + // setup data so that it will match + TaskAttribute attribute = taskData.getRoot().createAttribute("unusualIndexedAttribute"); + attribute.setValue(content); + + // update + context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor()); + + // verify index doesn't match search term + assertFalse(index.matches(repositoryTask, content)); + + // now make data indexable + attribute.getMetaData().putValue(TaskAttribute.META_INDEXED_AS_CONTENT, "true"); + // update + context.getDataManager().putSubmittedTaskData(repositoryTask, taskData, new DelegatingProgressMonitor()); + + // should now match + assertTrue(index.matches(repositoryTask, content)); + } + + /** + * Verify that multiple threads can concurrently use the index to find tasks, i.e. that no threads are blocked from + * finding tasks by other threads. + */ + @Test + public void testMultithreadedAccessOnFind() throws CoreException, InterruptedException, ExecutionException { + setupIndex(); + + final ITask repositoryTask = context.createRepositoryTask(); + + index.waitUntilIdle(); + index.setDefaultField(TaskListIndex.FIELD_CONTENT); + + final int nThreads = 10; + final int[] concurrencyLevel = new int[1]; + ExecutorService executorService = Executors.newFixedThreadPool(nThreads); + try { + Collection<Callable<Object>> tasks = new HashSet<Callable<Object>>(); + for (int x = 0; x < nThreads; ++x) { + tasks.add(new Callable<Object>() { + + public Object call() throws Exception { + final int[] hitCount = new int[1]; + index.find(repositoryTask.getSummary(), new TaskCollector() { + + @Override + public void collect(ITask task) { + synchronized (concurrencyLevel) { + ++concurrencyLevel[0]; + if (concurrencyLevel[0] < nThreads) { + try { + concurrencyLevel.wait(5000L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + concurrencyLevel.notifyAll(); + } + } + ++hitCount[0]; + } + }, 100); + return hitCount[0] == 1; + } + }); + } + List<Future<Object>> futures = executorService.invokeAll(tasks); + for (Future<Object> future : futures) { + assertEquals(Boolean.TRUE, future.get()); + } + Assert.assertEquals(nThreads, concurrencyLevel[0]); + } finally { + executorService.shutdownNow(); + } + } + + @Test + public void testRepositoryUrlChanged() throws InterruptedException, CoreException { + setupIndex(); + + ITask repositoryTask = context.createRepositoryTask(); + final String originalHandle = repositoryTask.getHandleIdentifier(); + + index.waitUntilIdle(); + + final String newUrl = context.getMockRepository().getRepositoryUrl() + "/changed"; + + context.refactorMockRepositoryUrl(newUrl); + + Assert.assertFalse(originalHandle.equals(repositoryTask.getHandleIdentifier())); + + index.waitUntilIdle(); + + Assert.assertTrue(index.matches( + repositoryTask, + TaskListIndex.FIELD_IDENTIFIER.getIndexKey() + ":" + + index.escapeFieldValue(repositoryTask.getHandleIdentifier()))); + } + }
\ No newline at end of file diff --git a/org.eclipse.mylyn.tasks.index.ui/src/org/eclipse/mylyn/internal/tasks/index/ui/IndexSearchHandler.java b/org.eclipse.mylyn.tasks.index.ui/src/org/eclipse/mylyn/internal/tasks/index/ui/IndexSearchHandler.java index 1c0c1b737..d6946a96a 100644 --- a/org.eclipse.mylyn.tasks.index.ui/src/org/eclipse/mylyn/internal/tasks/index/ui/IndexSearchHandler.java +++ b/org.eclipse.mylyn.tasks.index.ui/src/org/eclipse/mylyn/internal/tasks/index/ui/IndexSearchHandler.java @@ -1,263 +1,263 @@ -/*******************************************************************************
- * Copyright (c) 2011, 2012 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.ui;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.eclipse.jface.fieldassist.ContentProposalAdapter;
-import org.eclipse.jface.fieldassist.IContentProposal;
-import org.eclipse.jface.fieldassist.IContentProposalProvider;
-import org.eclipse.jface.fieldassist.TextContentAdapter;
-import org.eclipse.jface.layout.GridLayoutFactory;
-import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
-import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex;
-import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
-import org.eclipse.mylyn.internal.tasks.ui.search.AbstractSearchHandler;
-import org.eclipse.mylyn.tasks.core.IRepositoryManager;
-import org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema;
-import org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field;
-import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
-import org.eclipse.osgi.util.NLS;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.SelectionAdapter;
-import org.eclipse.swt.events.SelectionEvent;
-import org.eclipse.swt.widgets.Button;
-import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Text;
-import org.eclipse.ui.dialogs.PatternFilter;
-import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;
-
-/**
- * @author David Green
- */
-public class IndexSearchHandler extends AbstractSearchHandler {
-
- private class ContentProposalProvider implements IContentProposalProvider {
- public IContentProposal[] getProposals(String contents, int position) {
- List<IContentProposal> proposals = new ArrayList<IContentProposal>(10);
-
- String fieldPrefix = ""; //$NON-NLS-1$
- String prefix = ""; //$NON-NLS-1$
- if (position >= 0 && position <= contents.length()) {
- int i = position;
- while (i > 0 && !Character.isWhitespace(contents.charAt(i - 1)) && contents.charAt(i - 1) != ':') {
- --i;
- }
- if (i > 0 && contents.charAt(i - 1) == ':') {
- int fieldEnd = i - 1;
- int fieldStart = i - 1;
- while (fieldStart > 0 && Character.isLetter(contents.charAt(fieldStart - 1))) {
- --fieldStart;
- }
- fieldPrefix = contents.substring(fieldStart, fieldEnd);
- }
-
- prefix = contents.substring(i, position);
- }
-
- // if we have a field prefix
- if (fieldPrefix.length() > 0) {
- AbstractTaskSchema.Field indexField = computeIndexField(fieldPrefix);
-
- // if it's a person field then suggest
- // people from the task list
- if (indexField != null && TaskAttribute.TYPE_PERSON.equals(indexField.getType())) {
- computePersonProposals(proposals, prefix);
- }
-
- } else {
-
- // suggest field name prefixes
- for (Field field : index.getIndexedFields()) {
-
- // searching on URL is not useful
- if (field.equals(TaskListIndex.FIELD_IDENTIFIER)) {
- continue;
- }
-
- if (field.getIndexKey().startsWith(prefix)) {
- String description;
- if (TaskListIndex.FIELD_CONTENT.equals(field)) {
- description = Messages.IndexSearchHandler_hint_content;
- } else if (TaskListIndex.FIELD_PERSON.equals(field)) {
- description = Messages.IndexSearchHandler_hint_person;
- } else {
- description = NLS.bind(Messages.IndexSearchHandler_hint_generic, field.getLabel());
- }
- proposals.add(new ContentProposal(field.getIndexKey().substring(prefix.length()) + ":", //$NON-NLS-1$
- field.getIndexKey(), description));
-
- if (TaskAttribute.TYPE_DATE.equals(field.getType())
- || TaskAttribute.TYPE_DATETIME.equals(field.getType())) {
- computeDateRangeProposals(proposals, field);
- }
- }
- }
- }
-
- return proposals.toArray(new IContentProposal[proposals.size()]);
- }
-
- public void computeDateRangeProposals(List<IContentProposal> proposals, Field field) {
- // for date fields give suggestion of date range search
- String description;
- final Date now = new Date();
- final Date dateSearchUpperBound;
- final Date dateSearchOneWeekLowerBound;
- {
- GregorianCalendar calendar = new GregorianCalendar();
-
- calendar.setTime(now);
- calendar.add(Calendar.DAY_OF_WEEK, 1); // one day in future due to GMT conversion in index
- dateSearchUpperBound = calendar.getTime();
-
- calendar.setTime(now);
- calendar.add(Calendar.DAY_OF_WEEK, -7);
- dateSearchOneWeekLowerBound = calendar.getTime();
- }
-
- description = NLS.bind(Messages.IndexSearchHandler_Generic_date_range_search_1_week, field.getLabel());
-
- String label = NLS.bind(Messages.IndexSearchHandler_Past_week_date_range_label, field.getIndexKey());
-
- String queryText = index.computeQueryFieldDateRange(field, dateSearchOneWeekLowerBound,
- dateSearchUpperBound);
- proposals.add(new ContentProposal(queryText, label, description));
- }
-
- public void computePersonProposals(List<IContentProposal> proposals, String prefix) {
- Set<String> addresses = new TreeSet<String>();
-
- Collection<AbstractTask> allTasks = TasksUiPlugin.getTaskList().getAllTasks();
- for (AbstractTask task : allTasks) {
- addAddresses(addresses, task);
- }
-
- for (String address : addresses) {
- if (address.startsWith(prefix)) {
- proposals.add(new ContentProposal(address.substring(prefix.length()), address, null));
- }
- }
- }
-
- private void addAddresses(Set<String> addresses, AbstractTask task) {
- String name = task.getOwner();
- if (name != null && name.trim().length() > 0) {
- addresses.add(name.trim());
- }
- }
- }
-
- private static TaskListIndex theIndex;
-
- private static AtomicInteger referenceCount = new AtomicInteger();
-
- /**
- * When not null serves as flag indicating that theIndex is referenced, thus preventing bad behaviour if dispose is
- * called multiple times.
- */
- private TaskListIndex index;
-
- public IndexSearchHandler() {
- }
-
- public Field computeIndexField(String fieldPrefix) {
- for (Field field : index.getIndexedFields()) {
- if (field.getIndexKey().equals(fieldPrefix)) {
- return field;
- }
- }
- return null;
- }
-
- @Override
- public Composite createSearchComposite(Composite parent) {
- Composite container = new Composite(parent, SWT.NULL);
- GridLayoutFactory.swtDefaults().applyTo(container);
-
- final Button button = new Button(container, SWT.CHECK);
- button.setText(Messages.IndexSearchHandler_summaryOnly);
- button.setToolTipText(Messages.IndexSearchHandler_summaryOnly_tooltip);
- button.setSelection(true);
-
- button.addSelectionListener(new SelectionAdapter() {
- @Override
- public void widgetSelected(SelectionEvent e) {
- Field newDefaultField = button.getSelection()
- ? TaskListIndex.FIELD_SUMMARY
- : TaskListIndex.FIELD_CONTENT;
- index.setDefaultField(newDefaultField);
- fireFilterChanged();
- }
- });
-
- return container;
- }
-
- @Override
- public PatternFilter createFilter() {
- synchronized (IndexSearchHandler.class) {
- if (index == null) {
- if (theIndex == null) {
- final IRepositoryManager repositoryManager = TasksUiPlugin.getRepositoryManager();
- final File indexLocation = new File(TasksUiPlugin.getDefault().getDataDirectory(), ".taskListIndex"); //$NON-NLS-1$
-
- theIndex = new TaskListIndex(TasksUiPlugin.getTaskList(), TasksUiPlugin.getTaskDataManager(),
- repositoryManager, indexLocation);
-
- }
- index = theIndex;
- referenceCount.incrementAndGet();
- }
- }
- return new IndexedSubstringPatternFilter(index);
- }
-
- @Override
- public void adaptTextSearchControl(Text textControl) {
- IContentProposalProvider proposalProvider = new ContentProposalProvider();
- ContentAssistCommandAdapter adapter = new ContentAssistCommandAdapter(textControl, new TextContentAdapter(),
- proposalProvider, null, new char[0]);
- adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_INSERT);
-
- // if we decorate the control it lets the user know that they can use content assist...
- // BUT it looks pretty bad.
-// ControlDecoration controlDecoration = new ControlDecoration(textControl, (SWT.TOP | SWT.LEFT));
-// controlDecoration.setShowOnlyOnFocus(true);
-// FieldDecoration contentProposalImage = FieldDecorationRegistry.getDefault().getFieldDecoration(
-// FieldDecorationRegistry.DEC_CONTENT_PROPOSAL);
-// controlDecoration.setImage(contentProposalImage.getImage());
- }
-
- @Override
- public void dispose() {
- synchronized (IndexSearchHandler.class) {
- if (index != null) {
- index = null;
-
- if (referenceCount.decrementAndGet() == 0) {
- theIndex.close();
- theIndex = null;
- }
- }
- }
- }
-
-}
+/******************************************************************************* + * Copyright (c) 2011, 2012 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.ui; + +import java.io.File; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.fieldassist.TextContentAdapter; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.mylyn.internal.tasks.core.AbstractTask; +import org.eclipse.mylyn.internal.tasks.index.core.TaskListIndex; +import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin; +import org.eclipse.mylyn.internal.tasks.ui.search.AbstractSearchHandler; +import org.eclipse.mylyn.tasks.core.IRepositoryManager; +import org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema; +import org.eclipse.mylyn.tasks.core.data.AbstractTaskSchema.Field; +import org.eclipse.mylyn.tasks.core.data.TaskAttribute; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.dialogs.PatternFilter; +import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter; + +/** + * @author David Green + */ +public class IndexSearchHandler extends AbstractSearchHandler { + + private class ContentProposalProvider implements IContentProposalProvider { + public IContentProposal[] getProposals(String contents, int position) { + List<IContentProposal> proposals = new ArrayList<IContentProposal>(10); + + String fieldPrefix = ""; //$NON-NLS-1$ + String prefix = ""; //$NON-NLS-1$ + if (position >= 0 && position <= contents.length()) { + int i = position; + while (i > 0 && !Character.isWhitespace(contents.charAt(i - 1)) && contents.charAt(i - 1) != ':') { + --i; + } + if (i > 0 && contents.charAt(i - 1) == ':') { + int fieldEnd = i - 1; + int fieldStart = i - 1; + while (fieldStart > 0 && Character.isLetter(contents.charAt(fieldStart - 1))) { + --fieldStart; + } + fieldPrefix = contents.substring(fieldStart, fieldEnd); + } + + prefix = contents.substring(i, position); + } + + // if we have a field prefix + if (fieldPrefix.length() > 0) { + AbstractTaskSchema.Field indexField = computeIndexField(fieldPrefix); + + // if it's a person field then suggest + // people from the task list + if (indexField != null && TaskAttribute.TYPE_PERSON.equals(indexField.getType())) { + computePersonProposals(proposals, prefix); + } + + } else { + + // suggest field name prefixes + for (Field field : index.getIndexedFields()) { + + // searching on URL is not useful + if (field.equals(TaskListIndex.FIELD_IDENTIFIER)) { + continue; + } + + if (field.getIndexKey().startsWith(prefix)) { + String description; + if (TaskListIndex.FIELD_CONTENT.equals(field)) { + description = Messages.IndexSearchHandler_hint_content; + } else if (TaskListIndex.FIELD_PERSON.equals(field)) { + description = Messages.IndexSearchHandler_hint_person; + } else { + description = NLS.bind(Messages.IndexSearchHandler_hint_generic, field.getLabel()); + } + proposals.add(new ContentProposal(field.getIndexKey().substring(prefix.length()) + ":", //$NON-NLS-1$ + field.getIndexKey(), description)); + + if (TaskAttribute.TYPE_DATE.equals(field.getType()) + || TaskAttribute.TYPE_DATETIME.equals(field.getType())) { + computeDateRangeProposals(proposals, field); + } + } + } + } + + return proposals.toArray(new IContentProposal[proposals.size()]); + } + + public void computeDateRangeProposals(List<IContentProposal> proposals, Field field) { + // for date fields give suggestion of date range search + String description; + final Date now = new Date(); + final Date dateSearchUpperBound; + final Date dateSearchOneWeekLowerBound; + { + GregorianCalendar calendar = new GregorianCalendar(); + + calendar.setTime(now); + calendar.add(Calendar.DAY_OF_WEEK, 1); // one day in future due to GMT conversion in index + dateSearchUpperBound = calendar.getTime(); + + calendar.setTime(now); + calendar.add(Calendar.DAY_OF_WEEK, -7); + dateSearchOneWeekLowerBound = calendar.getTime(); + } + + description = NLS.bind(Messages.IndexSearchHandler_Generic_date_range_search_1_week, field.getLabel()); + + String label = NLS.bind(Messages.IndexSearchHandler_Past_week_date_range_label, field.getIndexKey()); + + String queryText = index.computeQueryFieldDateRange(field, dateSearchOneWeekLowerBound, + dateSearchUpperBound); + proposals.add(new ContentProposal(queryText, label, description)); + } + + public void computePersonProposals(List<IContentProposal> proposals, String prefix) { + Set<String> addresses = new TreeSet<String>(); + + Collection<AbstractTask> allTasks = TasksUiPlugin.getTaskList().getAllTasks(); + for (AbstractTask task : allTasks) { + addAddresses(addresses, task); + } + + for (String address : addresses) { + if (address.startsWith(prefix)) { + proposals.add(new ContentProposal(address.substring(prefix.length()), address, null)); + } + } + } + + private void addAddresses(Set<String> addresses, AbstractTask task) { + String name = task.getOwner(); + if (name != null && name.trim().length() > 0) { + addresses.add(name.trim()); + } + } + } + + private static TaskListIndex theIndex; + + private static AtomicInteger referenceCount = new AtomicInteger(); + + /** + * When not null serves as flag indicating that theIndex is referenced, thus preventing bad behaviour if dispose is + * called multiple times. + */ + private TaskListIndex index; + + public IndexSearchHandler() { + } + + public Field computeIndexField(String fieldPrefix) { + for (Field field : index.getIndexedFields()) { + if (field.getIndexKey().equals(fieldPrefix)) { + return field; + } + } + return null; + } + + @Override + public Composite createSearchComposite(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + GridLayoutFactory.swtDefaults().applyTo(container); + + final Button button = new Button(container, SWT.CHECK); + button.setText(Messages.IndexSearchHandler_summaryOnly); + button.setToolTipText(Messages.IndexSearchHandler_summaryOnly_tooltip); + button.setSelection(true); + + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Field newDefaultField = button.getSelection() + ? TaskListIndex.FIELD_SUMMARY + : TaskListIndex.FIELD_CONTENT; + index.setDefaultField(newDefaultField); + fireFilterChanged(); + } + }); + + return container; + } + + @Override + public PatternFilter createFilter() { + synchronized (IndexSearchHandler.class) { + if (index == null) { + if (theIndex == null) { + final IRepositoryManager repositoryManager = TasksUiPlugin.getRepositoryManager(); + final File indexLocation = new File(TasksUiPlugin.getDefault().getDataDirectory(), ".taskListIndex"); //$NON-NLS-1$ + + theIndex = new TaskListIndex(TasksUiPlugin.getTaskList(), TasksUiPlugin.getTaskDataManager(), + repositoryManager, indexLocation); + + } + index = theIndex; + referenceCount.incrementAndGet(); + } + } + return new IndexedSubstringPatternFilter(index); + } + + @Override + public void adaptTextSearchControl(Text textControl) { + IContentProposalProvider proposalProvider = new ContentProposalProvider(); + ContentAssistCommandAdapter adapter = new ContentAssistCommandAdapter(textControl, new TextContentAdapter(), + proposalProvider, null, new char[0]); + adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_INSERT); + + // if we decorate the control it lets the user know that they can use content assist... + // BUT it looks pretty bad. +// ControlDecoration controlDecoration = new ControlDecoration(textControl, (SWT.TOP | SWT.LEFT)); +// controlDecoration.setShowOnlyOnFocus(true); +// FieldDecoration contentProposalImage = FieldDecorationRegistry.getDefault().getFieldDecoration( +// FieldDecorationRegistry.DEC_CONTENT_PROPOSAL); +// controlDecoration.setImage(contentProposalImage.getImage()); + } + + @Override + public void dispose() { + synchronized (IndexSearchHandler.class) { + if (index != null) { + index = null; + + if (referenceCount.decrementAndGet() == 0) { + theIndex.close(); + theIndex = null; + } + } + } + } + +} |