blob: d2bd318eaf2579869e9cbae79e55bf9dabdd2329 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Tasktop Technologies.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Tasktop Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.tasks.ui.migrator;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.in;
import static com.google.common.collect.Iterables.any;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.mylyn.internal.tasks.ui.migrator.TaskPredicates.isQueryForConnector;
import static org.eclipse.mylyn.internal.tasks.ui.migrator.TaskPredicates.isTaskForConnector;
import static org.eclipse.mylyn.internal.tasks.ui.migrator.TaskPredicates.isTaskSynchronizing;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.CoreException;
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.core.runtime.SubMonitor;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.AbstractTaskCategory;
import org.eclipse.mylyn.internal.tasks.core.IRepositoryConstants;
import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
import org.eclipse.mylyn.internal.tasks.core.TaskList;
import org.eclipse.mylyn.internal.tasks.core.TaskTask;
import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryManager;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITask.SynchronizationState;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.sync.SynchronizationJob;
import org.eclipse.osgi.util.NLS;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
/**
* Allows users to migrate their data from an old connector to a new one for the same repository. Performs the following
* steps:
*
* <pre>
* * uses task list message service to prompt users to migrate
* * backs up the task list
* * automatically migrates repositories
* * directs user to manually migrate queries and click a button in the task list message to complete migration
* * once new queries have finished syncing, any tasks that are missing from the new queries are fetched by searching by task key
* * private data (context, notes, categories, scheduled dates, and private due dates) is automatically migrated for all tasks
* * all tasks are marked read except those which were incoming before migration
* * old repositories are deleted
* </pre>
*/
public class ConnectorMigrator {
private static final ImmutableSet<String> EXCLUDED_REPOSITORY_PROPERTIES = ImmutableSet.of(
IRepositoryConstants.PROPERTY_CONNECTOR_KIND, IRepositoryConstants.PROPERTY_SYNCTIMESTAMP,
IRepositoryConstants.PROPERTY_URL);
protected static class OldTaskState {
private final SynchronizationState syncState;
private final ITask oldTask;
public OldTaskState(ITask oldTask) {
this.oldTask = oldTask;
this.syncState = oldTask.getSynchronizationState();
}
public ITask getOldTask() {
return oldTask;
}
public SynchronizationState getSyncState() {
return syncState;
}
}
private final Map<String, String> connectorKinds;
private final String explanatoryText;
private final TasksState tasksState;
private List<String> connectorsToMigrate = ImmutableList.of();
private final ConnectorMigrationUi migrationUi;
private final Map<TaskRepository, TaskRepository> repositories = new HashMap<TaskRepository, TaskRepository>();
private final Table<TaskRepository, String, OldTaskState> oldTasksStates = HashBasedTable.create();
private Map<ITask, AbstractTaskCategory> categories;
private final JobListener syncTaskJobListener = new JobListener(new Runnable() {
@Override
public void run() {
completeMigration();
}
});
private boolean anyQueriesMigrated;
private boolean allQueriesMigrated = true;
public ConnectorMigrator(Map<String, String> connectorKinds, String explanatoryText, TasksState tasksState,
ConnectorMigrationUi migrationUi) {
checkArgument(!connectorKinds.isEmpty());
this.connectorKinds = connectorKinds;
this.explanatoryText = explanatoryText;
this.migrationUi = migrationUi;
this.tasksState = tasksState;
}
public Map<String, String> getConnectorKinds() {
return ImmutableMap.copyOf(connectorKinds);
}
public String getExplanatoryText() {
return explanatoryText;
}
public boolean needsMigration() {
for (Entry<String, String> entry : connectorKinds.entrySet()) {
String oldKind = entry.getKey();
String newKind = entry.getValue();
if (getRepositoryManager().getRepositoryConnector(oldKind) != null
&& getRepositoryManager().getRepositoryConnector(newKind) != null
&& !getRepositoryManager().getRepositories(oldKind).isEmpty()) {
return true;
}
}
return false;
}
public void setConnectorsToMigrate(List<String> connectors) {
checkArgument(connectorKinds.keySet().containsAll(connectors));
this.connectorsToMigrate = ImmutableList.copyOf(connectors);
}
protected void migrateConnectors(IProgressMonitor monitor) throws IOException {
final List<TaskRepository> failedValidation = new ArrayList<>();
List<TaskRepository> oldRepositories = gatherRepositoriesToMigrate(connectorsToMigrate);
monitor.beginTask(Messages.ConnectorMigrator_Migrating_repositories, oldRepositories.size() + 1);
getMigrationUi().backupTaskList(monitor);
for (TaskRepository repository : oldRepositories) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
monitor.subTask(NLS.bind(Messages.ConnectorMigrator_Migrating_X, repository.getRepositoryLabel()));
String kind = repository.getConnectorKind();
String newKind = getConnectorKinds().get(kind);
TaskRepository newRepository = getMigratedRepository(newKind, repository);
getRepositoryManager().addRepository(newRepository);
repositories.put(repository, newRepository);
Set<ITask> tasksToMigrate = Sets.filter(getTaskList().getTasks(repository.getRepositoryUrl()),
isTaskForConnector(repository.getConnectorKind()));
for (ITask task : tasksToMigrate) {
oldTasksStates.put(newRepository, task.getTaskKey(), new OldTaskState(task));
}
migrateQueries(repository, newRepository, monitor);
disconnect(repository);
monitor.worked(1);
}
Set<TaskRepository> newRepositories = ImmutableSet.copyOf(repositories.values());
monitor.beginTask(Messages.ConnectorMigrator_Validating_repository_connections, newRepositories.size());
for (TaskRepository newRepository : newRepositories) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
monitor.subTask(NLS.bind(Messages.ConnectorMigrator_Validating_connection_to_X,
newRepository.getRepositoryLabel()));
AbstractRepositoryConnector newConnector = getRepositoryManager()
.getRepositoryConnector(newRepository.getConnectorKind());
try {
newConnector.validateRepository(newRepository, monitor);
} catch (UnsupportedOperationException | CoreException e) {
failedValidation.add(newRepository);
}
monitor.worked(1);
}
monitor.done();
if (!failedValidation.isEmpty()) {
getMigrationUi().warnOfValidationFailure(failedValidation);
}
}
protected void migrateQueries(TaskRepository repository, TaskRepository newRepository, IProgressMonitor monitor) {
Set<RepositoryQuery> queriesForUrl = getTaskList().getRepositoryQueries(repository.getRepositoryUrl());
Set<RepositoryQuery> queries = Sets.filter(queriesForUrl, isQueryForConnector(repository.getConnectorKind()));
SubMonitor subMonitor = SubMonitor.convert(monitor, Messages.ConnectorMigrator_Migrating_Queries,
queries.size());
for (RepositoryQuery query : queries) {
RepositoryQuery migratedQuery = migrateQuery(query, repository, newRepository, subMonitor);
if (migratedQuery != null) {
getTaskList().addQuery(migratedQuery);
anyQueriesMigrated = true;
} else {
allQueriesMigrated = false;
}
subMonitor.worked(1);
}
}
/**
* Connectors can override to attempt to automatically migrate queries if possible.
*/
protected RepositoryQuery migrateQuery(RepositoryQuery query, TaskRepository repository,
TaskRepository newRepository, IProgressMonitor monitor) {
return null;
}
/**
* @return whether any queries have been migrated for any repository
*/
protected boolean anyQueriesMigrated() {
return anyQueriesMigrated;
}
/**
* @return whether all queries have been migrated for all migrated repositories; returns <code>true</code> if no
* repositories have yet been migrated
*/
protected boolean allQueriesMigrated() {
return allQueriesMigrated;
}
protected void disconnect(TaskRepository repository) {
repository.setOffline(true);
// we need to change the label so that the new repo doesn't have the same label, so that it can be edited
repository.setRepositoryLabel(
NLS.bind(Messages.ConnectorMigrator_X_Unsupported_do_not_delete, repository.getRepositoryLabel()));
Set<RepositoryQuery> queriesForUrl = getTaskList().getRepositoryQueries(repository.getRepositoryUrl());
for (RepositoryQuery query : Sets.filter(queriesForUrl, isQueryForConnector(repository.getConnectorKind()))) {
query.setAutoUpdate(false);// prevent error logged when Mylyn asks new connector to sync query for old connector
}
}
protected List<TaskRepository> gatherRepositoriesToMigrate(List<String> connectors) {
List<TaskRepository> oldRepositories = new ArrayList<TaskRepository>();
for (String kind : connectors) {
oldRepositories.addAll(getRepositoryManager().getRepositories(kind));
}
return oldRepositories;
}
protected TaskRepository getMigratedRepository(String newKind, TaskRepository oldRepository) {
String migratedRepositoryUrl = getMigratedRepositoryUrl(oldRepository);
TaskRepository newRepository = getRepositoryManager().getRepository(newKind, migratedRepositoryUrl);
if (newRepository == null) {
newRepository = migrateRepository(newKind, migratedRepositoryUrl, oldRepository);
}
return newRepository;
}
protected String getMigratedRepositoryUrl(TaskRepository oldRepository) {
return oldRepository.getRepositoryUrl();
}
protected TaskRepository migrateRepository(String newKind, String migratedRepositoryUrl,
TaskRepository oldRepository) {
TaskRepository newRepository = new TaskRepository(newKind, migratedRepositoryUrl);
for (Entry<String, String> entry : oldRepository.getProperties().entrySet()) {
if (!EXCLUDED_REPOSITORY_PROPERTIES.contains(entry.getKey())) {
newRepository.setProperty(entry.getKey(), entry.getValue());
}
}
for (AuthenticationType type : AuthenticationType.values()) {
AuthenticationCredentials credentials = oldRepository.getCredentials(type);
newRepository.setCredentials(type, credentials, oldRepository.getSavePassword(type));
}
return newRepository;
}
protected void migrateTasks(IProgressMonitor monitor) {
tasksState.getTaskActivityManager().deactivateActiveTask();
// Note: we're assuming the new connector uses different task IDs (and therefore different handle identifiers)
// from the old one. This may not be the case for Bugzilla.
for (Entry<TaskRepository, TaskRepository> entry : repositories.entrySet()) {
TaskRepository oldRepository = entry.getKey();
TaskRepository newRepository = entry.getValue();
monitor.subTask(NLS.bind(Messages.ConnectorMigrator_Migrating_tasks_for_X, newRepository));
AbstractRepositoryConnector newConnector = getRepositoryManager()
.getRepositoryConnector(newRepository.getConnectorKind());
Set<ITask> tasksToMigrate = Sets.filter(getTaskList().getTasks(oldRepository.getRepositoryUrl()),
isTaskForConnector(oldRepository.getConnectorKind()));
migrateTasks(tasksToMigrate, oldRepository, newRepository, newConnector, monitor);
}
monitor.subTask(Messages.ConnectorMigrator_Waiting_for_tasks_to_synchronize);
getSyncTaskJobListener().start();
while (!getSyncTaskJobListener().isComplete()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, e.getMessage(), e));
}
}
}
protected void migrateTasks(final Set<ITask> tasksToMigrate, final TaskRepository oldRepository,
final TaskRepository newRepository, final AbstractRepositoryConnector newConnector,
final IProgressMonitor monitor) {
ImmutableMap<String, ITask> tasksByKey = FluentIterable
.from(getTaskList().getTasks(newRepository.getRepositoryUrl()))
.filter(isTaskForConnector(newConnector.getConnectorKind()))
.uniqueIndex(new Function<ITask, String>() {
@Override
public String apply(ITask task) {
return task.getTaskKey();
}
});
final Map<AbstractTask, OldTaskState> migratedTasks = new HashMap<>();
Set<ITask> tasksToSynchronize = new HashSet<ITask>();
for (ITask oldTask : tasksToMigrate) {
String taskKey = oldTask.getTaskKey();
ITask newTask = tasksByKey.get(taskKey);
if (newTask == null) {
TaskData taskData = getTaskData(taskKey, newConnector, newRepository, monitor);
if (taskData != null) {
newTask = createTask(taskData, newRepository);
tasksToSynchronize.add(newTask);
}
}
if (newTask instanceof AbstractTask) {
OldTaskState oldTaskState = oldTasksStates.get(newRepository, oldTask.getTaskKey());
if (oldTaskState == null) {
oldTaskState = new OldTaskState(oldTask);
}
migratedTasks.put((AbstractTask) newTask, oldTaskState);
}
if (newTask instanceof AbstractTask && oldTask instanceof AbstractTask) {
migratePrivateData((AbstractTask) oldTask, (AbstractTask) newTask, monitor);
}
}
oldTasksStates.row(newRepository).clear();
migrateTaskContext(migratedTasks);
getMigrationUi().delete(tasksToMigrate, oldRepository, newRepository, monitor);
for (ITask task : tasksToSynchronize) {
getTaskList().addTask(task);
}
SynchronizationJob job = tasksState.getTaskJobFactory().createSynchronizeTasksJob(newConnector, newRepository,
tasksToSynchronize);
getSyncTaskJobListener().add(job, new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
while (any(migratedTasks.keySet(), isTaskSynchronizing())
&& System.currentTimeMillis() - start < MILLISECONDS.convert(4, HOURS)) {
try {
Thread.sleep(MILLISECONDS.convert(3, SECONDS));
} catch (InterruptedException e) {// NOSONAR
}
}
for (Entry<AbstractTask, OldTaskState> entry : migratedTasks.entrySet()) {
AbstractTask newTask = entry.getKey();
OldTaskState oldTask = entry.getValue();
newTask.setSynchronizationState(oldTask.getSyncState());
}
Set<RepositoryQuery> queries = getTaskList().getRepositoryQueries(newRepository.getRepositoryUrl());
if (!queries.isEmpty()) {
SynchronizationJob synchronizeQueriesJob = tasksState.getTaskJobFactory()
.createSynchronizeQueriesJob(newConnector, newRepository, queries);
synchronizeQueriesJob.schedule();
}
}
});
job.schedule();
}
private void migrateTaskContext(Map<AbstractTask, OldTaskState> taskStates) {
Map<ITask, ITask> tasks = taskStates.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getValue().getOldTask(), e -> e.getKey()));
TasksUiPlugin.getContextStore().moveContext(tasks);
}
protected void completeMigration() {
categories = null;
getMigrationUi().notifyMigrationComplete();
}
protected void migratePrivateData(AbstractTask oldTask, AbstractTask newTask, IProgressMonitor monitor) {
AbstractTaskCategory category = getCategories().get(oldTask);
if (category != null) {
getTaskList().addTask(newTask, category);
}
newTask.setNotes(oldTask.getNotes());
tasksState.getTaskActivityManager().setScheduledFor(newTask, oldTask.getScheduledForDate());
tasksState.getTaskActivityManager().setDueDate(newTask, oldTask.getDueDate());
newTask.setEstimatedTimeHours(oldTask.getEstimatedTimeHours());
}
protected ITask createTask(TaskData taskData, TaskRepository repository) {
return new TaskTask(repository.getConnectorKind(), repository.getRepositoryUrl(), taskData.getTaskId());
}
/**
* This method is used to support migrating tasks that are not contained in any migrated query.
*/
protected TaskData getTaskData(String taskKey, AbstractRepositoryConnector newConnector,
TaskRepository newRepository, IProgressMonitor monitor) {
try {
if (newConnector.supportsSearchByTaskKey(newRepository)) {
return newConnector.searchByTaskKey(newRepository, taskKey, monitor);
}
} catch (CoreException e) {
StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Failed to migrate task " //$NON-NLS-1$
+ taskKey + " for repository " + newRepository.getRepositoryLabel(), e)); //$NON-NLS-1$
}
return null;
}
public Map<String, String> getSelectedConnectors() {
return Maps.filterKeys(getConnectorKinds(), in(connectorsToMigrate));
}
protected TaskList getTaskList() {
return tasksState.getTaskList();
}
protected IRepositoryManager getRepositoryManager() {
return tasksState.getRepositoryManager();
}
/**
* @return The task categorization that existed the first time this method was called
*/
protected Map<ITask, AbstractTaskCategory> getCategories() {
if (categories == null) {
categories = new HashMap<ITask, AbstractTaskCategory>();
for (AbstractTaskCategory category : getTaskList().getCategories()) {
for (ITask task : category.getChildren()) {
categories.put(task, category);
}
}
}
return categories;
}
public ConnectorMigrationUi getMigrationUi() {
return migrationUi;
}
protected JobListener getSyncTaskJobListener() {
return syncTaskJobListener;
}
}