package com.googlecode.mylyn.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.mylyn.tasks.core.IRepositoryPerson;
import org.eclipse.mylyn.tasks.core.ITask.PriorityLevel;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.RepositoryResponse;
import org.eclipse.mylyn.tasks.core.RepositoryResponse.ResponseKind;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMetaData;
import org.eclipse.mylyn.tasks.core.data.TaskCommentMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import com.google.gdata.data.BaseEntry;
import com.google.gdata.data.Content;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.HtmlTextConstruct;
import com.google.gdata.data.Person;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.TextContent;
import com.google.gdata.data.projecthosting.BlockedOn;
import com.google.gdata.data.projecthosting.BlockedOnUpdate;
import com.google.gdata.data.projecthosting.Blocking;
import com.google.gdata.data.projecthosting.IssueCommentsEntry;
import com.google.gdata.data.projecthosting.IssueCommentsFeed;
import com.google.gdata.data.projecthosting.IssuesEntry;
import com.google.gdata.data.projecthosting.Label;
import com.google.gdata.data.projecthosting.Owner;
import com.google.gdata.data.projecthosting.OwnerUpdate;
import com.google.gdata.data.projecthosting.SendEmail;
import com.google.gdata.data.projecthosting.State;
import com.google.gdata.data.projecthosting.State.Value;
import com.google.gdata.data.projecthosting.Status;
import com.google.gdata.data.projecthosting.Summary;
import com.google.gdata.data.projecthosting.Updates;
import com.google.gdata.data.projecthosting.Username;
import com.googlecode.mylyn.core.client.IGoogleCodeClient;
import com.googlecode.mylyn.core.util.RepositoryUtils;
/**
* This class is responsible for retrieving and posting task data to an Google
* Code task repository.
*
* @see TaskData
*/
class GoogleCodeTaskDataHandler extends AbstractTaskDataHandler {
private static final String ATTRIBUTE_TYPE_DEFAULT = "Defect";
private static final String ATTRIBUTE_STATUS_DEFAULT = "New";
static final String GOOGLE_CODE_KEY = "googleCodeKey";
private final GoogleCodeRepositoryConnector connector;
/**
* Initializes a {@link GoogleCodeTaskDataHandler}.
*
* @param connector the Google Code connector to use
*/
public GoogleCodeTaskDataHandler(GoogleCodeRepositoryConnector connector) {
this.connector = connector;
}
@Override
public TaskAttributeMapper getAttributeMapper(TaskRepository repository) {
return new TaskAttributeMapper(repository);
}
@Override
public boolean initializeTaskData(TaskRepository repository, TaskData data,
ITaskMapping initializationData, IProgressMonitor monitor) throws CoreException {
createAttribute(data, GoogleCodeAttribute.TASK_KEY);
TaskAttribute statusAttribute = createAttribute(data, GoogleCodeAttribute.STATUS, ATTRIBUTE_STATUS_DEFAULT);
LabelUtils.fillStatusOptions(statusAttribute);
createAttribute(data, GoogleCodeAttribute.SUMMARY);
if (data.isNew()) {
createAttribute(data, GoogleCodeAttribute.DESCRIPTION_NEW);
} else {
createAttribute(data, GoogleCodeAttribute.DESCRIPTION_EXISTING);
}
TaskAttribute typeAttribute = createAttribute(data, GoogleCodeAttribute.TYPE, ATTRIBUTE_TYPE_DEFAULT);
LabelUtils.fillTypeOptions(typeAttribute);
TaskAttribute priority = createAttribute(data, GoogleCodeAttribute.PRIORITY);
LabelUtils.fillPriorityOptions(priority);
createAttribute(data, GoogleCodeAttribute.MILESTONE);
createAttribute(data, GoogleCodeAttribute.DATE_MODIFICATION);
createAttribute(data, GoogleCodeAttribute.DATE_CREATION);
createAttribute(data, GoogleCodeAttribute.USER_ASSIGNED);
createAttribute(data, GoogleCodeAttribute.URL);
createAttribute(data, GoogleCodeAttribute.STARS);
createAttribute(data, GoogleCodeAttribute.BLOCKED_ON);
if (!data.isNew()) {
createAttribute(data, GoogleCodeAttribute.COMMENT_NEW);
createAttribute(data, GoogleCodeAttribute.USER_REPORTER);
createAttribute(data, GoogleCodeAttribute.DATE_COMPLETION);
createAttribute(data, GoogleCodeAttribute.BLOCKINGS);
}
return true;
}
private TaskAttribute createAttribute(TaskData data, GoogleCodeAttribute key,
String attributeDefault) {
TaskAttribute attribute = createAttribute(data, key);
attribute.setValue(attributeDefault);
return attribute;
}
private static TaskAttribute createAttribute(TaskData data, GoogleCodeAttribute key) {
return createAttribute(data.getRoot(), key);
}
private static TaskAttribute createAttribute(TaskAttribute parent,
GoogleCodeAttribute googleCodeAttribute) {
TaskAttribute taskAttribute = parent.createAttribute(googleCodeAttribute.getKey());
TaskAttributeMetaData metaData = taskAttribute.getMetaData();
metaData.defaults();
metaData.setReadOnly(googleCodeAttribute.isReadOnly());
metaData.setKind(googleCodeAttribute.getKind());
metaData.setLabel(googleCodeAttribute.getLabel());
metaData.setType(googleCodeAttribute.getType());
metaData.putValue(GOOGLE_CODE_KEY, googleCodeAttribute.getGoogleCodeKey());
return taskAttribute;
}
@Override
public RepositoryResponse postTaskData(TaskRepository repository, TaskData taskData,
Set<TaskAttribute> oldAttributes, IProgressMonitor monitor) throws CoreException {
RepositoryUtils.assertLoggedIn(repository);
if (taskData.isNew()) {
return insertIssue(repository, taskData, monitor);
} else {
return updateIssue(repository, taskData, oldAttributes, monitor);
}
}
private RepositoryResponse insertIssue(TaskRepository repository, TaskData taskData,
IProgressMonitor monitor) throws CoreException {
IGoogleCodeClient client = getClient(repository);
IssuesEntry entry = new IssuesEntry();
entry.addLabel(new Label(getPriority(taskData)));
String summary = getStringValue(taskData, GoogleCodeAttribute.SUMMARY);
String description = getStringValue(taskData, GoogleCodeAttribute.DESCRIPTION_NEW);
entry.getAuthors().add(client.getCurrentUser());
String ownerName = getStringValue(taskData, GoogleCodeAttribute.USER_ASSIGNED);
if (!StringUtils.isEmpty(ownerName)) {
Owner owner = new Owner();
owner.setUsername(new Username(ownerName));
entry.setOwner(owner);
}
entry.setContent(new HtmlTextConstruct(description));
entry.setTitle(new PlainTextConstruct(summary));
entry.setStatus(new Status("New"));
entry.setSendEmail(new SendEmail("False"));
IssuesEntry created = client.createIssue(entry, monitor);
String issueId = getIssueId(created);
return new RepositoryResponse(ResponseKind.TASK_CREATED, issueId);
}
private RepositoryResponse updateIssue(TaskRepository repository, TaskData taskData,
Set<TaskAttribute> oldAttributes, IProgressMonitor monitor) throws CoreException {
IGoogleCodeClient client = getClient(repository);
IssueCommentsEntry entry = new IssueCommentsEntry();
Updates updates = new Updates();
String summary = getStringValue(taskData, GoogleCodeAttribute.SUMMARY);
updates.setSummary(new Summary(summary));
String issueId = getIssueId(taskData);
String comment = getNewComment(taskData);
String currentPriority = getPriority(taskData);
String oldPriority = getOldPriority(oldAttributes);
if (!currentPriority.equals(oldPriority)) {
if (oldPriority != null) {
updates.addLabel(new Label('-' + oldPriority));
}
updates.addLabel(new Label(currentPriority));
}
String ownerName = getStringValue(taskData, GoogleCodeAttribute.USER_ASSIGNED);
String oldOwner = getOldOwner(oldAttributes);
if (!(ObjectUtils.equals(ownerName, oldOwner) || (StringUtils.isEmpty(ownerName) && StringUtils
.isEmpty(oldOwner)))) {
OwnerUpdate ownerUpdate;
if (!StringUtils.isEmpty(ownerName)) {
ownerUpdate = new OwnerUpdate(ownerName);
} else {
// remove the owner
// FIXME broken, doesn't work, null doesn't work as well
ownerUpdate = new OwnerUpdate(StringUtils.EMPTY);
}
updates.setOwnerUpdate(ownerUpdate);
}
updates.setStatus(new Status(getStringValue(taskData, GoogleCodeAttribute.STATUS)));
computeBlockedOnDifference(taskData, oldAttributes, updates);
entry.getAuthors().add(client.getCurrentUser());
if (!StringUtils.isEmpty(comment)) {
entry.setContent(new HtmlTextConstruct(comment));
}
entry.setSendEmail(new SendEmail("False"));
entry.setUpdates(updates);
client.updateIssue(issueId, entry, monitor);
return new RepositoryResponse(ResponseKind.TASK_UPDATED, issueId);
}
private void computeBlockedOnDifference(TaskData taskData, Set<TaskAttribute> oldAttributes,
Updates updates) {
List<String> newBlockedOns = getNewBlockedOn(taskData);
List<String> oldBlockedOns = getOldBlockedOn(taskData, oldAttributes, newBlockedOns);
ArrayList<String> oldBlockOnsCopy = new ArrayList<String>(oldBlockedOns);
oldBlockedOns.removeAll(newBlockedOns);
newBlockedOns.removeAll(oldBlockOnsCopy);
for (String oldBlockon : oldBlockedOns) {
createBlockUpdate(oldBlockon, true, updates);
}
for (String newBlockon : newBlockedOns) {
createBlockUpdate(newBlockon, false, updates);
}
}
private List<String> getNewBlockedOn(TaskData taskData) {
TaskAttribute blockedOnAttribute = getAttribute(taskData, GoogleCodeAttribute.BLOCKED_ON);
List<String> newBlockedOns = taskData.getAttributeMapper().getValues(blockedOnAttribute);
if (newBlockedOns != null && newBlockedOns.size() > 0) {
String[] splittedIssueIds = newBlockedOns.get(0).split(",");
newBlockedOns.clear();
for (int i = 0; i < splittedIssueIds.length; i++) {
String string = splittedIssueIds[i].trim();
newBlockedOns.add(string);
}
} else {
newBlockedOns = Collections.emptyList();
}
return newBlockedOns;
}
private List<String> getOldBlockedOn(TaskData taskData, Set<TaskAttribute> oldAttributes,
List<String> newBlockedOns) {
TaskAttribute oldBlockedOnAttribute = getOldAttribute(GoogleCodeAttribute.BLOCKED_ON,
oldAttributes);
List<String> oldBlockedOns = null;
if (oldBlockedOnAttribute != null) {
TaskAttributeMapper attributeMapper = taskData.getAttributeMapper();
oldBlockedOns = attributeMapper.getValues(oldBlockedOnAttribute);
} else {
oldBlockedOns = Collections.emptyList();
}
return new ArrayList<String>(oldBlockedOns);
}
private void createBlockUpdate(String issueId, boolean remove, Updates updates) {
if (!"".equals(issueId)) {
String removeFlag = remove ? "-" : "";
BlockedOnUpdate blockedOnUpdate = new BlockedOnUpdate();
blockedOnUpdate.setValue(removeFlag + issueId);
updates.addBlockedOnUpdate(blockedOnUpdate);
}
}
private TaskAttribute getOldAttribute(GoogleCodeAttribute googleCodeAttribute,
Set<TaskAttribute> oldAttributes) {
for (TaskAttribute attribute : oldAttributes) {
if (attribute.getId().equals(googleCodeAttribute.getKey())) {
return attribute;
}
}
return null;
}
private String getOldPriority(Set<TaskAttribute> oldAttributes) {
TaskAttribute attribute = getOldAttribute(GoogleCodeAttribute.PRIORITY, oldAttributes);
if (attribute != null) {
return getPriority(attribute.getTaskData());
} else {
return null;
}
}
private String getOldOwner(Set<TaskAttribute> oldAttributes) {
TaskAttribute attribute = getOldAttribute(GoogleCodeAttribute.USER_ASSIGNED, oldAttributes);
if (attribute != null) {
return attribute.getValue();
} else {
return null;
}
}
private String getPriority(TaskData taskData) {
PriorityLevel level = PriorityLevel.fromString(getStringValue(taskData,
GoogleCodeAttribute.PRIORITY));
if (level != null) {
String priority = LabelUtils.convertToGoogle(level);
if (priority != null) {
return "Priority-" + priority;
}
}
return "Priority-" + LabelUtils.PRIORITY_MEDIUM;
}
private static String getNewComment(TaskData data) {
return getStringValue(data, GoogleCodeAttribute.COMMENT_NEW);
}
private static String getIssueId(TaskData data) {
return getStringValue(data, GoogleCodeAttribute.TASK_KEY);
}
private static String getStringValue(TaskData data, GoogleCodeAttribute attribute) {
return data.getAttributeMapper().getValue(getAttribute(data, attribute));
}
private IGoogleCodeClient getClient(TaskRepository repository) throws CoreException {
return connector.getClient(repository);
}
/**
* Transfers the data from an Google Code specific {@link IssuesEntry} to a
* Mylyn generic {@link TaskData}.
*
* @param repository the repository to use
* @param taskId the id of the task
* @param issueEntry the issue entry
* @param monitor the progress monitor
* @return a mylyn task data object with the data from the Google Code issue
* @throws CoreException if task data creation fails
*/
public TaskData updateTaskData(TaskRepository repository, IssuesEntry issueEntry,
IProgressMonitor monitor) throws CoreException {
String repositoryUrl = repository.getRepositoryUrl();
String issueId = getIssueId(issueEntry);
TaskData data = new TaskData(getAttributeMapper(repository), repository.getConnectorKind(),
repositoryUrl, issueId);
this.initializeTaskData(repository, data, null, monitor);
String title = issueEntry.getTitle().getPlainText();
setAttributeValue(data, GoogleCodeAttribute.SUMMARY, title);
setAttributeValue(data, GoogleCodeAttribute.USER_REPORTER, issueEntry.getAuthors().get(0).getName());
String summary = getPlainTextContent(issueEntry, true);
if (data.isNew()) {
setAttributeValue(data, GoogleCodeAttribute.DESCRIPTION_NEW, summary);
} else {
setAttributeValue(data, GoogleCodeAttribute.DESCRIPTION_EXISTING, summary);
}
setAttributeValue(data, GoogleCodeAttribute.TASK_KEY, issueId);
if (issueEntry.hasStatus()) {
// {Status value=Fixed}{Status value=Duplicate}{Status
// value=WontFix}{Status value=Accepted}
Status status = issueEntry.getStatus();
setAttributeValue(data, GoogleCodeAttribute.STATUS, status.getValue());
}
if (issueEntry.hasLabels()) {
// [{Label value=Type-Defect}, {Label value=Priority-Medium}, {Label
// value=Type-Enhancement}]
for (Label label : issueEntry.getLabels()) {
if (label.hasValue()) {
String value = label.getValue();
if (value.startsWith("Type-")) {
String type = value.substring("Type-".length());
setAttributeValue(data, GoogleCodeAttribute.TYPE, type);
} else if (value.startsWith("Priority-")) {
String priority = value.substring("Priority-".length());
PriorityLevel level = LabelUtils.convertToMylyn(priority);
if (level != null) {
setAttributeValue(data, GoogleCodeAttribute.PRIORITY, level.toString());
}
} else if (value.startsWith("Milestone-")) {
String milestone = value.substring("Milestone-".length());
setAttributeValue(data, GoogleCodeAttribute.MILESTONE, milestone);
}
}
}
}
setAttributeValue(data, GoogleCodeAttribute.DATE_CREATION, issueEntry.getPublished());
setAttributeValue(data, GoogleCodeAttribute.DATE_MODIFICATION, issueEntry.getUpdated());
if (issueEntry.hasOwner()) {
IRepositoryPerson owner = getPerson(data, issueEntry.getOwner());
setAttributeValue(data, GoogleCodeAttribute.USER_ASSIGNED, owner);
}
if (!data.isNew()) {
TaskAttribute url = getAttribute(data, GoogleCodeAttribute.URL);
url.setValue(this.connector.getTaskUrl(repository.getUrl(), issueId));
}
IGoogleCodeClient client = getClient(repository);
IssueCommentsFeed comments = client.getAllComments(issueId, monitor);
if (!data.isNew()) {
addComments(issueId, data, comments, monitor);
}
// Set the completion date, this allows Mylyn mark the issue as
// completed
// There is no API for this so we make an educated guess
State state = issueEntry.getState();
if (!data.isNew() && Value.CLOSED == state.getValue()) {
// find the last comment that set the issue status to the current
// value
// it would be better to find the last comment to set that state to
// closed
// but as long as we can't do that this will have to do
// initialize with issue creation date in case we don't find any
// comments
DateTime closingDate = issueEntry.getPublished();
for (IssueCommentsEntry comment : comments.getEntries()) {
Updates updates = comment.getUpdates();
if (updates != null && issueEntry.getStatus().equals(updates.getStatus())) {
closingDate = comment.getPublished();
}
}
setAttributeValue(data, GoogleCodeAttribute.DATE_COMPLETION, closingDate);
}
data.getAttributeMapper().setIntegerValue(getAttribute(data, GoogleCodeAttribute.STARS),
issueEntry.getStars().getValue());
List<BlockedOn> blockedOn = issueEntry.getBlockedOns();
List<String> rawBlockOns = new ArrayList<String>();
for (BlockedOn blocking : blockedOn) {
rawBlockOns.add(blocking.getId().getValue().toString());
}
setAttributeValue(data, GoogleCodeAttribute.BLOCKED_ON, rawBlockOns);
List<Blocking> blockings = issueEntry.getBlockings();
List<String> rawBlockings = new ArrayList<String>();
for (Blocking blocking : blockings) {
rawBlockings.add(blocking.getId().getValue().toString());
}
setAttributeValue(data, GoogleCodeAttribute.BLOCKINGS, rawBlockings);
return data;
}
private String getIssueId(IssuesEntry issueEntry) {
// id is something like
// http://code.google.com/feeds/issues/p/googlecode-mylyn-connector/issues/full/1
// and Mylyn doesn't like "-" in the id
return StringUtils.substringAfterLast(issueEntry.getId(), "/");
}
private static String getPlainTextContent(BaseEntry<?> issueEntry, boolean stripHtml) {
Content content = issueEntry.getContent();
if (content instanceof TextContent) {
// getTextContent() does the same thing and throws an
// IllegalStateException
// when the content is not TextContent
TextContent textContent = (TextContent) content;
if (textContent.getContent() != null
&& textContent.getContent() instanceof HtmlTextConstruct) {
HtmlTextConstruct html = (HtmlTextConstruct) textContent.getContent();
String htmlContent = html.getHtml();
if (stripHtml == true) {
return htmlContent.replaceAll("\\<.*?\\>", "");
} else {
return htmlContent;
}
}
}
return StringUtils.EMPTY;
}
private void addComments(String issueId, TaskData data, IssueCommentsFeed comments,
IProgressMonitor monitor) throws CoreException {
int i = 1;
for (IssueCommentsEntry issueComment : comments.getEntries()) {
String content = getPlainTextContent(issueComment, false);
if (!(StringUtils.isEmpty(content))) {
TaskAttribute attribute = data.getRoot().createAttribute(
TaskAttribute.PREFIX_COMMENT + i);
TaskCommentMapper taskComment = TaskCommentMapper.createFrom(attribute);
// Comment id is something like
// http://code.google.com/feeds/issues/p/googlecode-mylyn-connector/issues/1/comments/full/1
String commentId = StringUtils.substringAfterLast(issueComment.getId(), "/");
taskComment.setCommentId(commentId);
List<Person> authors = issueComment.getAuthors();
if (authors.size() == 1) {
IRepositoryPerson person = getPerson(data, authors.get(0));
taskComment.setAuthor(person);
}
taskComment.setNumber(i);
taskComment.setText(content);
taskComment.setCreationDate(new Date(issueComment.getPublished().getValue()));
taskComment.setUrl(commentId);
taskComment.applyTo(attribute);
}
i++;
}
}
public static TaskAttribute getAttribute(TaskData taskData, GoogleCodeAttribute key) {
return taskData.getRoot().getAttribute(key.getKey());
}
private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, String value) {
if (value != null) {
TaskAttribute attribute = getAttribute(data, key);
data.getAttributeMapper().setValue(attribute, value);
}
}
private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, DateTime dateTime) {
if (dateTime != null) {
TaskAttribute attribute = getAttribute(data, key);
data.getAttributeMapper().setDateValue(attribute, new Date(dateTime.getValue()));
}
}
private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, List<String> list) {
if (list != null) {
TaskAttribute attribute = getAttribute(data, key);
data.getAttributeMapper().setValues(attribute, list);
}
}
private static void setAttributeValue(TaskData data, GoogleCodeAttribute key,
IRepositoryPerson person) {
if (person != null) {
TaskAttribute attribute = getAttribute(data, key);
setAttributeValue(data, attribute, person);
}
}
private static void setAttributeValue(TaskData data, TaskAttribute attribute,
IRepositoryPerson person) {
if (person != null) {
data.getAttributeMapper().setRepositoryPerson(attribute, person);
}
}
private IRepositoryPerson getPerson(TaskData data, Person person) {
TaskRepository repository = data.getAttributeMapper().getTaskRepository();
String email = person.getEmail();
IRepositoryPerson repositoryPerson;
if (email != null) {
repositoryPerson = repository.createPerson(email);
} else {
// seems to be the case in at least one case
repositoryPerson = repository.createPerson(person.getName());
}
repositoryPerson.setName(person.getName());
return repositoryPerson;
}
private IRepositoryPerson getPerson(TaskData data, Owner owner) {
TaskRepository repository = data.getAttributeMapper().getTaskRepository();
IRepositoryPerson person = repository.createPerson(owner.getUsername().getValue());
return person;
}
}