/******************************************************************************* * Copyright (c) 2015 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.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; } }