blob: dcd003bcad7fd3620685c5d0688b6315a256e85d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2008 Steffen Pingel 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.trac.core;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.trac.core.client.AbstractWikiHandler;
import org.eclipse.mylyn.internal.trac.core.client.ITracClient;
import org.eclipse.mylyn.internal.trac.core.client.ITracWikiClient;
import org.eclipse.mylyn.internal.trac.core.client.ITracClient.Version;
import org.eclipse.mylyn.internal.trac.core.model.TracPriority;
import org.eclipse.mylyn.internal.trac.core.model.TracSearch;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket;
import org.eclipse.mylyn.internal.trac.core.util.TracUtil;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.TaskRepositoryLocationFactory;
import org.eclipse.mylyn.tasks.core.ITask.PriorityLevel;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.data.TaskRelation;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
/**
* @author Steffen Pingel
*/
public class TracRepositoryConnector extends AbstractRepositoryConnector {
public enum TaskKind {
DEFECT, ENHANCEMENT, TASK;
public static TaskKind fromString(String type) {
if (type == null) {
return null;
}
if (type.equals("Defect")) { //$NON-NLS-1$
return DEFECT;
}
if (type.equals("Enhancement")) { //$NON-NLS-1$
return ENHANCEMENT;
}
if (type.equals("Task")) { //$NON-NLS-1$
return TASK;
}
return null;
}
public static TaskKind fromType(String type) {
if (type == null) {
return null;
}
if (type.equals("defect")) { //$NON-NLS-1$
return DEFECT;
}
if (type.equals("enhancement")) { //$NON-NLS-1$
return ENHANCEMENT;
}
if (type.equals("task")) { //$NON-NLS-1$
return TASK;
}
return null;
}
@Override
public String toString() {
switch (this) {
case DEFECT:
return "Defect"; //$NON-NLS-1$
case ENHANCEMENT:
return "Enhancement"; //$NON-NLS-1$
case TASK:
return "Task"; //$NON-NLS-1$
default:
return ""; //$NON-NLS-1$
}
}
}
public enum TaskStatus {
ASSIGNED, CLOSED, NEW, REOPENED;
public static TaskStatus fromStatus(String status) {
if (status == null) {
return null;
}
if (status.equals("new")) { //$NON-NLS-1$
return NEW;
}
if (status.equals("assigned")) { //$NON-NLS-1$
return ASSIGNED;
}
if (status.equals("reopened")) { //$NON-NLS-1$
return REOPENED;
}
if (status.equals("closed")) { //$NON-NLS-1$
return CLOSED;
}
return null;
}
public String toStatusString() {
switch (this) {
case NEW:
return "new"; //$NON-NLS-1$
case ASSIGNED:
return "assigned"; //$NON-NLS-1$
case REOPENED:
return "reopened"; //$NON-NLS-1$
case CLOSED:
return "closed"; //$NON-NLS-1$
default:
return ""; //$NON-NLS-1$
}
}
@Override
public String toString() {
switch (this) {
case NEW:
return "New"; //$NON-NLS-1$
case ASSIGNED:
return "Assigned"; //$NON-NLS-1$
case REOPENED:
return "Reopened"; //$NON-NLS-1$
case CLOSED:
return "Closed"; //$NON-NLS-1$
default:
return ""; //$NON-NLS-1$
}
}
}
public enum TracPriorityLevel {
BLOCKER, CRITICAL, MAJOR, MINOR, TRIVIAL;
public static TracPriorityLevel fromPriority(String priority) {
if (priority == null) {
return null;
}
if (priority.equals("blocker")) { //$NON-NLS-1$
return BLOCKER;
}
if (priority.equals("critical")) { //$NON-NLS-1$
return CRITICAL;
}
if (priority.equals("major")) { //$NON-NLS-1$
return MAJOR;
}
if (priority.equals("minor")) { //$NON-NLS-1$
return MINOR;
}
if (priority.equals("trivial")) { //$NON-NLS-1$
return TRIVIAL;
}
return null;
}
public PriorityLevel toPriorityLevel() {
switch (this) {
case BLOCKER:
return PriorityLevel.P1;
case CRITICAL:
return PriorityLevel.P2;
case MAJOR:
return PriorityLevel.P3;
case MINOR:
return PriorityLevel.P4;
case TRIVIAL:
return PriorityLevel.P5;
default:
return null;
}
}
@Override
public String toString() {
switch (this) {
case BLOCKER:
return "blocker"; //$NON-NLS-1$
case CRITICAL:
return "critical"; //$NON-NLS-1$
case MAJOR:
return "major"; //$NON-NLS-1$
case MINOR:
return "minor"; //$NON-NLS-1$
case TRIVIAL:
return "trivial"; //$NON-NLS-1$
default:
return null;
}
}
}
private final static String CLIENT_LABEL = Messages.TracRepositoryConnector_TRAC_SUPPORTS_0_9_OR_0_10_THROUGH_WEB_AND_XML_RPC;
private final static Date DEFAULT_COMPLETION_DATE = new Date(0);
private static int TASK_PRIORITY_LEVELS = 5;
public static final String TASK_KEY_SUPPORTS_SUBTASKS = "SupportsSubtasks"; //$NON-NLS-1$
public static final String TASK_KEY_UPDATE_DATE = "UpdateDate"; //$NON-NLS-1$
public static String getDisplayUsername(TaskRepository repository) {
AuthenticationCredentials credentials = repository.getCredentials(AuthenticationType.REPOSITORY);
if (credentials != null && credentials.getUserName().length() > 0) {
return ITracClient.DEFAULT_USERNAME;
}
return repository.getUserName();
}
public static PriorityLevel getTaskPriority(String tracPriority) {
if (tracPriority != null) {
TracPriorityLevel priority = TracPriorityLevel.fromPriority(tracPriority);
if (priority != null) {
return priority.toPriorityLevel();
}
}
return PriorityLevel.getDefault();
}
public static PriorityLevel getTaskPriority(String priority, TracPriority[] tracPriorities) {
if (priority != null && tracPriorities != null && tracPriorities.length > 0) {
int minValue = tracPriorities[0].getValue();
int range = tracPriorities[tracPriorities.length - 1].getValue() - minValue;
for (TracPriority tracPriority : tracPriorities) {
if (priority.equals(tracPriority.getName())) {
float relativeValue = (float) (tracPriority.getValue() - minValue) / range;
int value = (int) (relativeValue * TASK_PRIORITY_LEVELS) + 1;
return PriorityLevel.fromLevel(value);
}
}
}
return getTaskPriority(priority);
}
public static int getTicketId(String taskId) throws CoreException {
try {
return Integer.parseInt(taskId);
} catch (NumberFormatException e) {
throw new CoreException(new Status(IStatus.ERROR, TracCorePlugin.ID_PLUGIN, IStatus.OK,
"Invalid ticket id: " + taskId, e)); //$NON-NLS-1$
}
}
static List<String> getAttributeValues(TaskData data, String attributeId) {
TaskAttribute attribute = data.getRoot().getMappedAttribute(attributeId);
if (attribute != null) {
return attribute.getValues();
} else {
return Collections.emptyList();
}
}
static String getAttributeValue(TaskData data, String attributeId) {
TaskAttribute attribute = data.getRoot().getMappedAttribute(attributeId);
if (attribute != null) {
return attribute.getValue();
} else {
return ""; //$NON-NLS-1$
}
}
public static boolean hasAttachmentSupport(TaskRepository repository, ITask task) {
return Version.XML_RPC.name().equals(repository.getVersion());
}
public static boolean hasChangedSince(TaskRepository repository) {
return Version.XML_RPC.name().equals(repository.getVersion());
}
public static boolean hasRichEditor(TaskRepository repository) {
return Version.XML_RPC.name().equals(repository.getVersion());
}
public static boolean hasRichEditor(TaskRepository repository, ITask task) {
return hasRichEditor(repository);
}
public static boolean isCompleted(String tracStatus) {
TaskStatus taskStatus = TaskStatus.fromStatus(tracStatus);
return taskStatus == TaskStatus.CLOSED;
}
private final TracAttachmentHandler attachmentHandler = new TracAttachmentHandler(this);
private TracClientManager clientManager;
private File repositoryConfigurationCacheFile;
private final TracTaskDataHandler taskDataHandler = new TracTaskDataHandler(this);
private TaskRepositoryLocationFactory taskRepositoryLocationFactory = new TaskRepositoryLocationFactory();
private final TracWikiHandler wikiHandler = new TracWikiHandler(this);
public TracRepositoryConnector() {
if (TracCorePlugin.getDefault() != null) {
TracCorePlugin.getDefault().setConnector(this);
IPath path = TracCorePlugin.getDefault().getRepostioryAttributeCachePath();
this.repositoryConfigurationCacheFile = path.toFile();
}
}
public TracRepositoryConnector(File repositoryConfigurationCacheFile) {
this.repositoryConfigurationCacheFile = repositoryConfigurationCacheFile;
}
@Override
public boolean canCreateNewTask(TaskRepository repository) {
return true;
}
@Override
public boolean canCreateTaskFromKey(TaskRepository repository) {
return true;
}
@Override
public boolean canSynchronizeTask(TaskRepository taskRepository, ITask task) {
return hasRichEditor(taskRepository, task);
}
@Override
public TracAttachmentHandler getTaskAttachmentHandler() {
return attachmentHandler;
}
public synchronized TracClientManager getClientManager() {
if (clientManager == null) {
clientManager = new TracClientManager(repositoryConfigurationCacheFile, taskRepositoryLocationFactory);
}
return clientManager;
}
@Override
public String getConnectorKind() {
return TracCorePlugin.CONNECTOR_KIND;
}
@Override
public String getLabel() {
return CLIENT_LABEL;
}
@Override
public TaskData getTaskData(TaskRepository repository, String taskId, IProgressMonitor monitor)
throws CoreException {
return taskDataHandler.getTaskData(repository, taskId, monitor);
}
@Override
public TracTaskDataHandler getTaskDataHandler() {
return taskDataHandler;
}
@Override
public String getRepositoryUrlFromTaskUrl(String url) {
if (url == null) {
return null;
}
int index = url.lastIndexOf(ITracClient.TICKET_URL);
return index == -1 ? null : url.substring(0, index);
}
@Override
public String getTaskIdFromTaskUrl(String url) {
if (url == null) {
return null;
}
int index = url.lastIndexOf(ITracClient.TICKET_URL);
return index == -1 ? null : url.substring(index + ITracClient.TICKET_URL.length());
}
@Override
public String getTaskIdPrefix() {
return "#"; //$NON-NLS-1$
}
public TaskRepositoryLocationFactory getTaskRepositoryLocationFactory() {
return taskRepositoryLocationFactory;
}
@Override
public String getTaskUrl(String repositoryUrl, String taskId) {
return repositoryUrl + ITracClient.TICKET_URL + taskId;
}
public AbstractWikiHandler getWikiHandler() {
return wikiHandler;
}
public boolean hasWiki(TaskRepository repository) {
// check the access mode to validate Wiki support
ITracClient client = getClientManager().getTracClient(repository);
if (client instanceof ITracWikiClient) {
return true;
}
return false;
}
@Override
public IStatus performQuery(TaskRepository repository, IRepositoryQuery query, TaskDataCollector resultCollector,
ISynchronizationSession session, IProgressMonitor monitor) {
try {
monitor.beginTask(Messages.TracRepositoryConnector_Querying_repository, IProgressMonitor.UNKNOWN);
TracSearch search = TracUtil.toTracSearch(query);
if (search == null) {
return new RepositoryStatus(repository.getRepositoryUrl(), IStatus.ERROR, TracCorePlugin.ID_PLUGIN,
RepositoryStatus.ERROR_REPOSITORY, "The query is invalid: \"" + query.getUrl() + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
ITracClient client;
try {
client = getClientManager().getTracClient(repository);
List<TracTicket> tickets = new ArrayList<TracTicket>();
client.search(search, tickets, monitor);
Map<String, ITask> taskById = null;
client.updateAttributes(monitor, false);
for (TracTicket ticket : tickets) {
TaskData taskData = taskDataHandler.createTaskDataFromTicket(client, repository, ticket, monitor);
taskData.setPartial(true);
if (session != null && !session.isFullSynchronization() && hasRichEditor(repository)) {
if (taskById == null) {
taskById = new HashMap<String, ITask>();
for (ITask task : session.getTasks()) {
taskById.put(task.getTaskId(), task);
}
}
ITask task = taskById.get(ticket.getId() + ""); //$NON-NLS-1$
if (task != null && hasTaskChanged(repository, task, taskData)) {
session.markStale(task);
}
}
resultCollector.accept(taskData);
}
} catch (Throwable e) {
return TracCorePlugin.toStatus(e, repository);
}
return Status.OK_STATUS;
} finally {
monitor.done();
}
}
@Override
public void postSynchronization(ISynchronizationSession event, IProgressMonitor monitor) throws CoreException {
try {
monitor.beginTask("", 1); //$NON-NLS-1$
if (event.isFullSynchronization() && event.getStatus() == null) {
Date date = getSynchronizationTimestamp(event);
if (date != null) {
event.getTaskRepository().setSynchronizationTimeStamp(TracUtil.toTracTime(date) + ""); //$NON-NLS-1$
}
}
} finally {
monitor.done();
}
}
private Date getSynchronizationTimestamp(ISynchronizationSession event) {
Date mostRecent = new Date(0);
Date mostRecentTimeStamp = TracUtil.parseDate(event.getTaskRepository().getSynchronizationTimeStamp());
for (ITask task : event.getChangedTasks()) {
Date taskModifiedDate = task.getModificationDate();
if (taskModifiedDate != null && taskModifiedDate.after(mostRecent)) {
mostRecent = taskModifiedDate;
mostRecentTimeStamp = task.getModificationDate();
}
}
return mostRecentTimeStamp;
}
@Override
public void preSynchronization(ISynchronizationSession session, IProgressMonitor monitor) throws CoreException {
monitor = Policy.monitorFor(monitor);
try {
monitor.beginTask(Messages.TracRepositoryConnector_Getting_changed_tasks, IProgressMonitor.UNKNOWN);
if (!session.isFullSynchronization()) {
return;
}
// there are no Trac tasks in the task list, skip contacting the repository
if (session.getTasks().isEmpty()) {
return;
}
TaskRepository repository = session.getTaskRepository();
if (!TracRepositoryConnector.hasChangedSince(repository)) {
// always run the queries for web mode
return;
}
if (repository.getSynchronizationTimeStamp() == null
|| repository.getSynchronizationTimeStamp().length() == 0) {
for (ITask task : session.getTasks()) {
session.markStale(task);
}
return;
}
Date since = new Date(0);
try {
since = TracUtil.parseDate(Integer.parseInt(repository.getSynchronizationTimeStamp()));
} catch (NumberFormatException e) {
}
try {
ITracClient client = getClientManager().getTracClient(repository);
Set<Integer> ids = client.getChangedTickets(since, monitor);
if (ids.isEmpty()) {
// repository is unchanged
session.setNeedsPerformQueries(false);
return;
}
if (ids.size() == 1) {
// getChangedTickets() is expected to always return at least
// one ticket because
// the repository synchronization timestamp is set to the
// most recent modification date
Integer id = ids.iterator().next();
Date lastChanged = client.getTicketLastChanged(id, monitor);
if (since.equals(lastChanged)) {
// repository didn't actually change
session.setNeedsPerformQueries(false);
return;
}
}
for (ITask task : session.getTasks()) {
Integer id = getTicketId(task.getTaskId());
if (ids.contains(id)) {
session.markStale(task);
}
}
} catch (OperationCanceledException e) {
throw e;
} catch (Exception e) {
// TODO catch TracException
throw new CoreException(TracCorePlugin.toStatus(e, repository));
}
} finally {
monitor.done();
}
}
public synchronized void setTaskRepositoryLocationFactory(
TaskRepositoryLocationFactory taskRepositoryLocationFactory) {
this.taskRepositoryLocationFactory = taskRepositoryLocationFactory;
if (this.clientManager != null) {
clientManager.setTaskRepositoryLocationFactory(taskRepositoryLocationFactory);
}
}
public void stop() {
if (clientManager != null) {
clientManager.writeCache();
}
}
@Override
public void updateRepositoryConfiguration(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
try {
ITracClient client = getClientManager().getTracClient(repository);
client.updateAttributes(monitor, true);
} catch (Exception e) {
throw new CoreException(RepositoryStatus.createStatus(repository.getRepositoryUrl(), IStatus.WARNING,
TracCorePlugin.ID_PLUGIN, "Could not update attributes")); //$NON-NLS-1$
}
}
@Override
public void updateTaskFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData) {
TaskMapper mapper = getTaskMapping(taskData);
mapper.applyTo(task);
if (isCompleted(mapper.getStatus())) {
Date modificationDate = mapper.getModificationDate();
if (modificationDate == null) {
// web mode does not set a date
modificationDate = DEFAULT_COMPLETION_DATE;
}
task.setCompletionDate(modificationDate);
} else {
task.setCompletionDate(null);
}
task.setUrl(taskRepository.getRepositoryUrl() + ITracClient.TICKET_URL + taskData.getTaskId());
task.setAttribute(TASK_KEY_SUPPORTS_SUBTASKS, Boolean.toString(taskDataHandler.supportsSubtasks(taskData)));
if (!taskData.isPartial()) {
Date date = task.getModificationDate();
task.setAttribute(TASK_KEY_UPDATE_DATE, (date != null) ? TracUtil.toTracTime(date) + "" : null); //$NON-NLS-1$
}
}
@Override
public boolean hasTaskChanged(TaskRepository taskRepository, ITask task, TaskData taskData) {
TaskMapper mapper = getTaskMapping(taskData);
if (taskData.isPartial()) {
return mapper.hasChanges(task);
} else {
Date repositoryDate = mapper.getModificationDate();
Date localDate = TracUtil.parseDate(task.getAttribute(TASK_KEY_UPDATE_DATE));
if (repositoryDate != null && repositoryDate.equals(localDate)) {
return false;
}
return true;
}
}
@Override
public Collection<TaskRelation> getTaskRelations(TaskData taskData) {
TaskAttribute attribute = taskData.getRoot().getAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY);
if (attribute != null) {
List<TaskRelation> result = new ArrayList<TaskRelation>();
StringTokenizer t = new StringTokenizer(attribute.getValue(), ", "); //$NON-NLS-1$
while (t.hasMoreTokens()) {
result.add(TaskRelation.subtask(t.nextToken()));
}
return result;
}
return Collections.emptySet();
}
@Override
public TracTaskMapper getTaskMapping(TaskData taskData) {
TaskRepository taskRepository = taskData.getAttributeMapper().getTaskRepository();
ITracClient client = (taskRepository != null) ? getClientManager().getTracClient(taskRepository) : null;
return new TracTaskMapper(taskData, client);
}
}