/******************************************************************************* * Copyright (c) 2006, 2010 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: * Steffen Pingel - initial API and implementation * Benjamin Muskalla (Tasktop Technologies) - support for deleting tasks *******************************************************************************/ 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.ITracClient.Version; import org.eclipse.mylyn.internal.trac.core.client.ITracWikiClient; import org.eclipse.mylyn.internal.trac.core.client.TracException; import org.eclipse.mylyn.internal.trac.core.model.TracComment; 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.ITask.PriorityLevel; 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.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.TaskHistory; import org.eclipse.mylyn.tasks.core.data.TaskMapper; import org.eclipse.mylyn.tasks.core.data.TaskRelation; import org.eclipse.mylyn.tasks.core.data.TaskRevision; import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession; /** * @author Steffen Pingel * @author Benjamin Muskalla */ public class TracRepositoryConnector extends AbstractRepositoryConnector { public enum TaskKind { DEFECT, ENHANCEMENT, TASK, STORY; 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; } if (type.equals("Story")) { //$NON-NLS-1$ return STORY; } return null; } public static TaskKind fromType(String type) { if (type == null) { return null; } if (type.equals("defect") || type.equals("error")) { //$NON-NLS-1$ //$NON-NLS-2$ return DEFECT; } if (type.equals("enhancement")) { //$NON-NLS-1$ return ENHANCEMENT; } if (type.equals("task")) { //$NON-NLS-1$ return TASK; } if (type.equals("story")) { //$NON-NLS-1$ return STORY; } 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$ case STORY: return "Story"; //$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 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 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 Messages.TracRepositoryConnector_Trac_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$ } search.setMax(TaskDataCollector.MAX_HITS); ITracClient client; try { Map taskById = null; client = getClientManager().getTracClient(repository); client.updateAttributes(monitor, false); if (session != null && session.isFullSynchronization() && hasRichEditor(repository) && !session.getTasks().isEmpty()) { // performance optimization: only fetch task ids, all changed tasks have already been marked stale by preSynchronization() List ticketIds = new ArrayList(); client.searchForTicketIds(search, ticketIds, monitor); for (Integer id : ticketIds) { if (taskById == null) { taskById = new HashMap(); for (ITask task : session.getTasks()) { taskById.put(task.getTaskId(), task); } } TaskData taskData = new TaskData(taskDataHandler.getAttributeMapper(repository), TracCorePlugin.CONNECTOR_KIND, repository.getRepositoryUrl(), id + ""); //$NON-NLS-1$ taskData.setPartial(true); TaskAttribute attribute = TracTaskDataHandler.createAttribute(taskData, TracAttribute.ID); attribute.setValue(id + ""); //$NON-NLS-1$ resultCollector.accept(taskData); } } else { List tickets = new ArrayList(); client.search(search, tickets, monitor); 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(); for (ITask task : session.getTasks()) { taskById.put(task.getTaskId(), task); } } // preSyncronization() only handles full synchronizations ITask task = taskById.get(ticket.getId() + ""); //$NON-NLS-1$ if (task != null && hasTaskChanged(repository, task, taskData)) { session.markStale(task); } } resultCollector.accept(taskData); } } } catch (OperationCanceledException e) { throw e; } 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 ids = client.getChangedTickets(since, monitor); // if (CoreUtil.TEST_MODE) { // System.err.println(" preSynchronization(): since=" + since.getTime() + ",changed=" + ids); //$NON-NLS-1$ //$NON-NLS-2$ // } 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 (CoreUtil.TEST_MODE) { // System.err.println(" preSynchronization(): since=" + since.getTime() + ", lastChanged=" + lastChanged.getTime()); //$NON-NLS-1$ //$NON-NLS-2$ // } 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 (OperationCanceledException e) { throw e; } catch (Throwable e) { throw new CoreException(TracCorePlugin.toStatus(e, repository)); } } @Override public void updateTaskFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData) { TaskMapper mapper = getTaskMapping(taskData); mapper.applyTo(task); String status = mapper.getStatus(); if (status != null) { 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()); if (!taskData.isPartial()) { task.setAttribute(TASK_KEY_SUPPORTS_SUBTASKS, Boolean.toString(taskDataHandler.supportsSubtasks(taskData))); 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 getTaskRelations(TaskData taskData) { TaskAttribute attribute = taskData.getRoot().getAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY); if (attribute != null) { List result = new ArrayList(); 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); } @Override public boolean canGetTaskHistory(TaskRepository repository, ITask task) { return Version.XML_RPC.name().equals(repository.getVersion()); } @Override public TaskHistory getTaskHistory(TaskRepository repository, ITask task, IProgressMonitor monitor) throws CoreException { try { ITracClient client = getClientManager().getTracClient(repository); List comments = client.getComments(getTicketId(task.getTaskId()), monitor); TaskHistory history = new TaskHistory(repository, task); TaskRevision revision = null; for (TracComment comment : comments) { String id = comment.getCreated().getTime() + ""; //$NON-NLS-1$ if (revision == null || !id.equals(revision.getId())) { revision = new TaskRevision(id, comment.getCreated(), repository.createPerson(comment.getAuthor())); history.add(revision); } TracAttribute attribute = TracAttribute.getByTracKey(comment.getField()); if (attribute != null) { String fieldName = attribute.toString(); if (fieldName.endsWith(":")) { //$NON-NLS-1$ fieldName = fieldName.substring(0, fieldName.length() - 1); } TaskRevision.Change change = new TaskRevision.Change(attribute.getTracKey(), fieldName, comment.getOldValue(), comment.getNewValue()); revision.add(change); } } return history; } catch (OperationCanceledException e) { throw e; } catch (Throwable e) { throw new CoreException(TracCorePlugin.toStatus(e, repository)); } } @Override public boolean canDeleteTask(TaskRepository repository, ITask task) { return hasRichEditor(repository); } @Override public IStatus deleteTask(TaskRepository repository, ITask task, IProgressMonitor monitor) throws CoreException { monitor = Policy.monitorFor(monitor); ITracClient client = getClientManager().getTracClient(repository); try { client.deleteTicket(getTicketId(task.getTaskId()), monitor); } catch (TracException e) { throw new CoreException(TracCorePlugin.toStatus(e, repository)); } return Status.OK_STATUS; } }