Skip to main content
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Davis2016-07-29 23:31:42 +0000
committerGerrit Code Review @ Eclipse.org2016-08-11 23:58:27 +0000
commit87d261761bc06e87322fdd61d94b264f451c4518 (patch)
tree2e4996abe8f90b75b4f416a73815e9e59150d8ef
parent47a134c4d308149cba23f62df67cb8e4b19c2db8 (diff)
downloadorg.eclipse.mylyn.tasks-87d261761bc06e87322fdd61d94b264f451c4518.tar.gz
org.eclipse.mylyn.tasks-87d261761bc06e87322fdd61d94b264f451c4518.tar.xz
org.eclipse.mylyn.tasks-87d261761bc06e87322fdd61d94b264f451c4518.zip
498906: provide ability for users to migrate to new versions of
connectors Change-Id: I6980b07ab7e984f4c7ea40fd8649093196d25755 Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=498906
-rw-r--r--org.eclipse.mylyn.tasks.ui.tests/META-INF/MANIFEST.MF4
-rw-r--r--org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizardTest.java261
-rw-r--r--org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUiTest.java272
-rw-r--r--org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizardTest.java247
-rw-r--r--org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigratorTest.java607
-rw-r--r--org.eclipse.mylyn.tasks.ui/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizard.java254
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUi.java312
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizard.java216
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrator.java426
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/DefaultTasksState.java24
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/JobListener.java73
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/Messages.java61
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TaskPredicates.java91
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TasksState.java78
-rw-r--r--org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/messages.properties46
16 files changed, 2972 insertions, 1 deletions
diff --git a/org.eclipse.mylyn.tasks.ui.tests/META-INF/MANIFEST.MF b/org.eclipse.mylyn.tasks.ui.tests/META-INF/MANIFEST.MF
index 376a98582..cc88e96b7 100644
--- a/org.eclipse.mylyn.tasks.ui.tests/META-INF/MANIFEST.MF
+++ b/org.eclipse.mylyn.tasks.ui.tests/META-INF/MANIFEST.MF
@@ -12,7 +12,9 @@ Require-Bundle: org.junit;bundle-version="4.8.2",
org.hamcrest;bundle-version="[1.0.0,2.0.0)",
org.eclipse.equinox.security,
org.eclipse.mylyn.tasks.core,
- org.eclipse.mylyn.tasks.tests
+ org.eclipse.mylyn.tasks.tests,
+ org.eclipse.mylyn.tests.util
Export-Package: org.eclipse.mylyn.internal.tasks.ui.editors;x-internal:=true,
+ org.eclipse.mylyn.internal.tasks.ui.migrator;x-internal:=true,
org.eclipse.mylyn.tasks.ui.editors,
org.eclipse.mylyn.tasks.ui.wizards
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizardTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizardTest.java
new file mode 100644
index 000000000..580224e31
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizardTest.java
@@ -0,0 +1,261 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Tasktop EULA
+ * which accompanies this distribution, and is available at
+ * http://tasktop.com/legal
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui.migrator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.mylyn.commons.workbench.WorkbenchUtil;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
+import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
+import org.eclipse.mylyn.internal.tasks.ui.migrator.CompleteConnectorMigrationWizard.MapContentProvider;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.TaskElementLabelProvider;
+import org.eclipse.mylyn.tests.util.TestFixture;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class CompleteConnectorMigrationWizardTest {
+
+ private CompleteConnectorMigrationWizard wizard;
+
+ private ConnectorMigrator migrator;
+
+ private ConnectorMigrationUi migrationUi;
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setUp() {
+ DefaultTasksState tasksState = new DefaultTasksState();
+ migrationUi = spy(new ConnectorMigrationUi(TaskListView.getFromActivePerspective(),
+ TasksUiPlugin.getBackupManager(), tasksState));
+ doNothing().when(migrationUi).warnOfValidationFailure((List<TaskRepository>) any(List.class));
+ doNothing().when(migrationUi).notifyMigrationComplete();
+ migrator = spy(new ConnectorMigrator(ImmutableMap.of("mock", "mock.new"), "", tasksState, migrationUi));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ TestFixture.resetTaskList();
+ }
+
+ @Test
+ public void addPages() {
+ createWizard(new CompleteConnectorMigrationWizard(migrator));
+ assertEquals(2, wizard.getPageCount());
+ }
+
+ @Test
+ public void firstPage() {
+ IWizardContainer container = createWizard(new CompleteConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ assertEquals("Have You Recreated Your Queries?", firstPage.getTitle());
+ assertEquals(
+ "Migration will remove your old queries. Please ensure you have created the new queries you want. "
+ + "Your old and new queries are shown below and you can edit them by double-clicking.",
+ firstPage.getMessage());
+ assertTrue(firstPage.getControl() instanceof Composite);
+ Composite control = (Composite) firstPage.getControl();
+ assertEquals(4, control.getChildren().length);
+ assertTrue(control.getChildren()[0] instanceof Label);
+ assertTrue(control.getChildren()[1] instanceof Label);
+ assertTrue(control.getChildren()[2] instanceof Tree);
+ assertTrue(control.getChildren()[3] instanceof Tree);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Test
+ public void queryTreeShowsOnlySelectedConnectors() {
+ migrator = new ConnectorMigrator(ImmutableMap.of("mock", "mock.new", "kind", "kind.new"), "",
+ new DefaultTasksState(), migrationUi);
+ migrator.setConnectorsToMigrate(ImmutableList.of("kind"));
+ createWizard(new CompleteConnectorMigrationWizard(migrator));
+ ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+ verify(wizard, times(2)).createRepositoryQueryMap(captor.capture());
+ assertEquals(ImmutableSet.of("kind"), ImmutableSet.copyOf(captor.getAllValues().get(0)));
+ assertEquals(ImmutableSet.of("kind.new"), ImmutableSet.copyOf(captor.getAllValues().get(1)));
+
+ migrator = new ConnectorMigrator(ImmutableMap.of("mock", "mock.new", "kind", "kind.new"), "",
+ new DefaultTasksState(), migrationUi);
+ migrator.setConnectorsToMigrate(ImmutableList.of("mock", "kind"));
+ createWizard(new CompleteConnectorMigrationWizard(migrator));
+ captor = ArgumentCaptor.forClass(Collection.class);
+ verify(wizard, times(2)).createRepositoryQueryMap(captor.capture());
+ assertEquals(ImmutableSet.of("mock", "kind"), ImmutableSet.copyOf(captor.getAllValues().get(0)));
+ assertEquals(ImmutableSet.of("mock.new", "kind.new"), ImmutableSet.copyOf(captor.getAllValues().get(1)));
+ }
+
+ @Test
+ public void secondPage() {
+ IWizardContainer container = createWizard(new CompleteConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ IWizardPage secondPage = firstPage.getNextPage();
+ assertEquals("Complete Migration", secondPage.getTitle());
+ assertEquals("Clicking finish will migrate your tasks and private data. This may take a while.",
+ secondPage.getMessage());
+ assertTrue(secondPage.getControl() instanceof Composite);
+ Composite control = (Composite) secondPage.getControl();
+ assertEquals(1, control.getChildren().length);
+ assertTrue(control.getChildren()[0] instanceof Text);
+ String text = ((Text) control.getChildren()[0]).getText();
+ assertTrue(text.contains("When you click finish, your context, scheduled dates, private notes and other data "
+ + "will be migrated to the new connectors. Any tasks in your task list that are not included in the new "
+ + "queries you created will be downloaded using the new connectors. The old tasks, "
+ + "queries, and repositories will be deleted."));
+ assertTrue(text.contains("This may take a while. You should not use the task list or task editor while this is happening. "
+ + "You will be prompted when migration is complete."));
+ assertTrue(text.contains("You will be able to "
+ + "undo the migration by selecting \"Restore Tasks from History\" in the Task List view menu and choosing the "
+ + "connector-migration-*.zip file stored in <workspace>/.metadata/.mylyn/backup. This will restore your task "
+ + "list and repositories to the state they were in before the migration, but any data stored by 3rd party "
+ + "plugins for Mylyn may be lost"));
+ }
+
+ @Test
+ public void performFinish() throws InvocationTargetException, InterruptedException, IOException {
+ createWizard(new CompleteConnectorMigrationWizard(migrator));
+ assertTrue(wizard.performFinish());
+ }
+
+ @Test
+ public void isPageComplete() throws Exception {
+ IWizardContainer container = createWizard(new CompleteConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ IWizardPage secondPage = firstPage.getNextPage();
+ assertTrue(firstPage.isPageComplete());
+ assertFalse(secondPage.isPageComplete());
+
+ container.showPage(secondPage);
+ assertTrue(firstPage.isPageComplete());
+ assertTrue(secondPage.isPageComplete());
+ }
+
+ @Test
+ public void createQueryTree() throws Exception {
+ TaskRepository repository = createRepository("mock", "http://mock");
+ Map<TaskRepository, ? extends Set<RepositoryQuery>> queries = ImmutableMap.of(repository,
+ ImmutableSet.of(new RepositoryQuery("mock", "mock")));
+ TreeViewer viewer = new CompleteConnectorMigrationWizard(migrator).createQueryTree(WorkbenchUtil.getShell(),
+ queries);
+ assertEquals(queries, viewer.getInput());
+ assertTrue(viewer.getContentProvider() instanceof MapContentProvider);
+ assertTrue(viewer.getLabelProvider() instanceof TaskElementLabelProvider);
+ assertNotNull(((TaskElementLabelProvider) viewer.getLabelProvider()).getImage(repository));
+ assertEquals("http://mock", ((TaskElementLabelProvider) viewer.getLabelProvider()).getText(repository));
+
+ assertEquals(0, viewer.getExpandedElements().length);
+ spinEventLoop();
+ assertEquals(1, viewer.getExpandedElements().length);
+ }
+
+ @Test
+ public void createRepositoryQueryMap() throws Exception {
+ TaskRepository repository1 = createRepository("mock", "http://mock");
+ TaskRepository repository2 = createRepository("mock", "http://mock2");
+ ImmutableSet<TaskRepository> repositories = ImmutableSet.of(repository1, repository2);
+ RepositoryQuery query1 = createQuery(repository1);
+ RepositoryQuery query2 = createQuery(repository1);
+ RepositoryQuery query3 = createQuery(repository2);
+ RepositoryQuery query4 = createQuery(repository2);
+
+ Map<TaskRepository, Set<RepositoryQuery>> map = //
+ new CompleteConnectorMigrationWizard(migrator).createRepositoryQueryMap(ImmutableList.of("mock"));
+ assertEquals(repositories, map.keySet());
+ assertEquals(ImmutableSet.of(query1, query2), map.get(repository1));
+ assertEquals(ImmutableSet.of(query3, query4), map.get(repository2));
+ }
+
+ @Test
+ public void createRepositoryQueryMapExcludesRepositoryWithNoQueries() throws Exception {
+ TaskRepository repository = createRepository("mock", "http://mock");
+ createRepository("mock", "http://mock2");
+ RepositoryQuery query = createQuery(repository);
+
+ Map<TaskRepository, Set<RepositoryQuery>> map = //
+ new CompleteConnectorMigrationWizard(migrator).createRepositoryQueryMap(ImmutableList.of("mock"));
+ assertEquals(ImmutableSet.of(repository), map.keySet());
+ assertEquals(ImmutableSet.of(query), map.get(repository));
+ }
+
+ @Test
+ public void createRepositoryQueryMapMigratedQuery() throws Exception {
+ TaskRepository repository = createRepository("mock", "http://mock");
+ TaskRepository migratedRepository = createRepository("mock-new", "http://mock");
+ RepositoryQuery query = createQuery(repository);
+ RepositoryQuery migratedQuery = createQuery(migratedRepository);
+
+ Map<TaskRepository, Set<RepositoryQuery>> map = //
+ new CompleteConnectorMigrationWizard(migrator).createRepositoryQueryMap(ImmutableList.of("mock"));
+ assertEquals(ImmutableSet.of(repository), map.keySet());
+ assertEquals(ImmutableSet.of(query), map.get(repository));
+
+ map = new CompleteConnectorMigrationWizard(migrator).createRepositoryQueryMap(ImmutableList.of("mock-new"));
+ assertEquals(ImmutableSet.of(migratedRepository), map.keySet());
+ assertEquals(ImmutableSet.of(migratedQuery), map.get(migratedRepository));
+ }
+
+ protected TaskRepository createRepository(String kind, String url) {
+ TaskRepository repository = new TaskRepository(kind, url);
+ migrator.getRepositoryManager().addRepository(repository);
+ return repository;
+ }
+
+ protected RepositoryQuery createQuery(TaskRepository repository) {
+ RepositoryQuery query = new RepositoryQuery(repository.getConnectorKind(), repository.getConnectorKind()
+ + repository.getRepositoryUrl() + Math.random());
+ query.setRepositoryUrl(repository.getRepositoryUrl());
+ TasksUiPlugin.getTaskList().addQuery(query);
+ return query;
+ }
+
+ private IWizardContainer createWizard(CompleteConnectorMigrationWizard wiz) {
+ wizard = spy(wiz);
+ WizardDialog dialog = new WizardDialog(WorkbenchUtil.getShell(), wizard);
+ dialog.create();
+ IWizardContainer container = spy(wizard.getContainer());
+ when(wizard.getContainer()).thenReturn(container);
+ return container;
+ }
+
+ private void spinEventLoop() {
+ while (Display.getCurrent().readAndDispatch()) {
+ }
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUiTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUiTest.java
new file mode 100644
index 000000000..18330c454
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUiTest.java
@@ -0,0 +1,272 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Tasktop EULA
+ * which accompanies this distribution, and is available at
+ * http://tasktop.com/legal
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui.migrator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.mylyn.internal.commons.notifications.feed.ServiceMessage;
+import org.eclipse.mylyn.internal.tasks.ui.TaskListBackupManager;
+import org.eclipse.mylyn.internal.tasks.ui.migrator.ConnectorMigrationUi.CompleteMigrationJob;
+import org.eclipse.mylyn.internal.tasks.ui.notifications.TaskListServiceMessageControl;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
+import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
+import org.eclipse.mylyn.tasks.core.IRepositoryManager;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.widgets.Display;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class ConnectorMigrationUiTest {
+
+ private ConnectorMigrator migrator;
+
+ private final TaskListView taskList = mock(TaskListView.class);
+
+ private final TaskListBackupManager backupManager = mock(TaskListBackupManager.class);
+
+ private final ConnectorMigrationUi migrationUi = spy(
+ new ConnectorMigrationUi(taskList, backupManager, new DefaultTasksState()));
+
+ private CompleteMigrationJob completeMigrationJob;
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setUp() {
+ doNothing().when(migrationUi).warnOfValidationFailure((List<TaskRepository>) any(List.class));
+ doNothing().when(migrationUi).notifyMigrationComplete();
+ }
+
+ @After
+ public void tearDown() {
+ if (completeMigrationJob != null) {
+ completeMigrationJob.dispose();
+ // otherwise it will keep rescheduling itself and interfere with other tests
+ }
+ }
+
+ @Test
+ public void promptToMigrate() {
+ ServiceMessage message = openMigrationPrompt(Window.OK);
+
+ assertEquals("End of Connector Support", message.getTitle());
+ assertTrue(message.getDescription().contains("<a href=\"migrate\">Click here</a>"));
+ assertEquals(Dialog.DLG_IMG_MESSAGE_INFO, message.getImage());
+ assertFalse(message.openLink("foo"));
+ verify(migrationUi, never()).createMigrationWizard(any(ConnectorMigrator.class));
+ verify(migrationUi, never()).createPromptToCompleteMigrationJob(any(ConnectorMigrator.class));
+
+ message.openLink("migrate");
+ }
+
+ @Test
+ public void finishMigrationWizard() throws Exception {
+ ServiceMessage message = openMigrationPrompt(Window.OK);
+ assertTrue(message.openLink("migrate"));
+ verify(migrationUi).createPromptToCompleteMigrationJob(any(ConnectorMigrator.class));
+ }
+
+ @Test
+ public void cancelMigrationWizard() throws Exception {
+ ServiceMessage message = openMigrationPrompt(Window.CANCEL);
+ assertFalse(message.openLink("migrate"));
+ verify(migrationUi, never()).createPromptToCompleteMigrationJob(any(ConnectorMigrator.class));
+ }
+
+ @Test
+ public void promptToMigrateOpensSecondMessage() {
+ ServiceMessage message = openMigrationPrompt(Window.OK);
+ Job job = mock(Job.class);
+ doReturn(job).when(migrationUi).createPromptToCompleteMigrationJob(any(ConnectorMigrator.class));
+
+ message.openLink("migrate");
+ verify(migrationUi).createMigrationWizard(any(ConnectorMigrator.class));
+ verify(migrationUi).createPromptToCompleteMigrationJob(any(ConnectorMigrator.class));
+ verify(job).schedule();
+ }
+
+ @Test
+ public void promptToCompleteMigration() throws InterruptedException {
+ ServiceMessage message = openCompleteMigrationPrompt(Window.OK);
+
+ assertEquals("Connector Migration", message.getTitle());
+ assertTrue(message.getDescription().contains("<a href=\"complete-migration\">complete migration</a>"));
+ assertEquals(Dialog.DLG_IMG_MESSAGE_WARNING, message.getImage());
+ assertFalse(message.openLink("foo"));
+ verify(migrationUi, never()).createCompleteMigrationWizard(any(ConnectorMigrator.class));
+
+ message.openLink("complete-migration");
+ verify(migrationUi).createCompleteMigrationWizard(any(ConnectorMigrator.class));
+ }
+
+ @Test
+ public void finishCompleteMigrationWizard() throws Exception {
+ ServiceMessage message = openCompleteMigrationPrompt(Window.OK);
+ assertTrue(message.openLink("complete-migration"));
+ }
+
+ @Test
+ public void cancelCompleteMigrationWizard() throws Exception {
+ ServiceMessage message = openCompleteMigrationPrompt(Window.CANCEL);
+ assertFalse(message.openLink("complete-migration"));
+ }
+
+ @Test
+ public void secondMessageReopensAfterDelay() throws Exception {
+ ServiceMessage message = openCompleteMigrationPrompt(Window.OK);
+ TaskListServiceMessageControl messageControl = createMessageControl();
+
+ Thread.sleep(2100);
+ ServiceMessage message2 = captureMessage(messageControl);
+ assertTrue(message2 != message);
+
+ Thread.sleep(2100);
+ ServiceMessage message3 = captureMessage(messageControl);
+ assertTrue(message3 != message2);
+
+ message3.openLink("complete-migration");
+ Thread.sleep(2100);
+ ServiceMessage message4 = captureMessage(messageControl);
+ assertTrue(message4 == message3);
+ }
+
+ @Test
+ public void backupTaskList() throws Exception {
+ ImmutableMap<String, String> kinds = ImmutableMap.of("mock", "mock.new");
+ TaskRepository repository = new TaskRepository("mock", "http://mock");
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, ImmutableSet.of(repository)));
+ IProgressMonitor monitor = mock(IProgressMonitor.class);
+ migrationUi.backupTaskList(monitor);
+ InOrder inOrder = inOrder(backupManager, migrationUi, migrator, monitor);
+ inOrder.verify(monitor).subTask("Backing up task list");
+ inOrder.verify(migrationUi).getBackupFileName(any(Date.class));
+ inOrder.verify(backupManager).backupNow(true);
+ inOrder.verify(monitor).worked(1);
+ }
+
+ @Test
+ public void getBackupFileName() throws Exception {
+ Date date = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ cal.set(Calendar.MILLISECOND, 0);
+ date = cal.getTime();
+ String fileName = migrationUi.getBackupFileName(date);
+ Matcher m = Pattern.compile("connector-migration-(\\d{4}_\\d{2}_\\d{2}_\\d{6}).zip").matcher(fileName);
+ Matcher dateFormatMatcher = Pattern.compile("\\d{4}-\\d{2}-\\d{2}-\\d{6}.zip").matcher(fileName);
+ Matcher oldDateFormatMatcher = Pattern.compile("\\d{4}-\\d{2}-\\d{2}").matcher(fileName);
+ assertTrue(m.matches());
+ assertFalse(dateFormatMatcher.find());
+ assertFalse(oldDateFormatMatcher.find());
+ Date fileNameTime = new SimpleDateFormat("yyyy_MM_dd_HHmmss").parse(m.group(1));
+ assertEquals(date, fileNameTime);
+ }
+
+ /**
+ * @param returnCode
+ * the return code of the wizard that opens when the link in the prompt is clicked
+ */
+ private ServiceMessage openMigrationPrompt(int returnCode) {
+ TaskListServiceMessageControl messageControl = createMessageControl();
+ migrator = spy(
+ new ConnectorMigrator(ImmutableMap.of("mock", "mock.new"), "", new DefaultTasksState(), migrationUi));
+ WizardDialog wizard = mock(WizardDialog.class);
+ when(wizard.open()).thenReturn(returnCode);
+ doReturn(wizard).when(migrationUi).createMigrationWizard(any(ConnectorMigrator.class));
+ migrationUi.promptToMigrate(migrator);
+ return captureMessage(messageControl);
+ }
+
+ /**
+ * @param returnCode
+ * the return code of the wizard that opens when the link in the prompt is clicked
+ */
+ private ServiceMessage openCompleteMigrationPrompt(int returnCode) throws InterruptedException {
+ TaskListServiceMessageControl messageControl = createMessageControl();
+ migrator = spy(
+ new ConnectorMigrator(ImmutableMap.of("mock", "mock.new"), "", new DefaultTasksState(), migrationUi));
+ when(migrationUi.getCompletionPromptFrequency()).thenReturn(2);
+ WizardDialog wizard = mock(WizardDialog.class);
+ when(wizard.open()).thenReturn(returnCode);
+ doReturn(wizard).when(migrationUi).createCompleteMigrationWizard(any(ConnectorMigrator.class));
+ completeMigrationJob = (CompleteMigrationJob) migrationUi.createPromptToCompleteMigrationJob(migrator);
+ assertTrue(completeMigrationJob.isSystem());
+ assertFalse(completeMigrationJob.isUser());
+ completeMigrationJob.schedule();
+ completeMigrationJob.join();
+ return captureMessage(messageControl);
+ }
+
+ private TaskListServiceMessageControl createMessageControl() {
+ TaskListServiceMessageControl messageControl = mock(TaskListServiceMessageControl.class);
+ when(taskList.getServiceMessageControl()).thenReturn(messageControl);
+ return messageControl;
+ }
+
+ private ServiceMessage captureMessage(TaskListServiceMessageControl messageControl) {
+ spinEventLoop();
+ ArgumentCaptor<ServiceMessage> messageCaptor = ArgumentCaptor.forClass(ServiceMessage.class);
+ verify(messageControl, atLeastOnce()).setMessage(messageCaptor.capture());
+ ServiceMessage message = messageCaptor.getValue();
+ return message;
+ }
+
+ private void spinEventLoop() {
+ while (Display.getCurrent().readAndDispatch()) {
+ }
+ }
+
+ private ConnectorMigrator createMigrator(boolean hasOldConnector, boolean hasNewConnector,
+ Map<String, String> kinds, Set<TaskRepository> repositories) {
+ IRepositoryManager manager = mock(IRepositoryManager.class);
+ AbstractRepositoryConnector connector = mock(AbstractRepositoryConnector.class);
+ ConnectorMigrator migrator = new ConnectorMigrator(kinds, "", new DefaultTasksState(), migrationUi);
+ when(manager.getRepositories("mock")).thenReturn(repositories);
+ if (hasOldConnector) {
+ when(manager.getRepositoryConnector("mock")).thenReturn(connector);
+ when(manager.getRepositoryConnector("kind")).thenReturn(connector);
+ }
+ if (hasNewConnector) {
+ when(manager.getRepositoryConnector("mock.new")).thenReturn(connector);
+ when(manager.getRepositoryConnector("kind.new")).thenReturn(connector);
+ }
+ return migrator;
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizardTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizardTest.java
new file mode 100644
index 000000000..8ffc39009
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizardTest.java
@@ -0,0 +1,247 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Tasktop EULA
+ * which accompanies this distribution, and is available at
+ * http://tasktop.com/legal
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui.migrator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.mylyn.commons.workbench.WorkbenchUtil;
+import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryManager;
+import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
+import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
+import org.eclipse.mylyn.tasks.core.IRepositoryManager;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.PlatformUI;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class ConnectorMigrationWizardTest {
+ public class TestConnectorMigrationWizard extends ConnectorMigrationWizard {
+ private TestConnectorMigrationWizard(ConnectorMigrator migrator) {
+ super(migrator);
+ }
+
+ @Override
+ protected CheckboxTreeViewer createConnectorList(Composite parent, List<String> kinds) {
+ return new CheckboxTreeViewer(parent) {
+ {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ fireCheckStateChanged(null);
+ }
+ });
+ }
+
+ @Override
+ public Object[] getCheckedElements() {
+ return new String[] { "foo", "bar" };
+ }
+ };
+ }
+ }
+
+ private ConnectorMigrationWizard wizard;
+
+ private final ConnectorMigrationUi migrationUi = spy(new ConnectorMigrationUi(
+ TaskListView.getFromActivePerspective(), TasksUiPlugin.getBackupManager(), new DefaultTasksState()));
+
+ private ConnectorMigrator migrator;
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setUp() {
+ doNothing().when(migrationUi).warnOfValidationFailure((List<TaskRepository>) any(List.class));
+ doNothing().when(migrationUi).notifyMigrationComplete();
+ createMigrator(ImmutableMap.of("mock", "mock.new"));
+ }
+
+ @Test
+ public void addPages() {
+ createWizard(new ConnectorMigrationWizard(migrator));
+ assertEquals(2, wizard.getPageCount());
+ }
+
+ @Test
+ public void firstPage() {
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ assertEquals("End of Connector Support", firstPage.getTitle());
+ assertEquals(
+ "Support is ending for some connectors, but replacement connectors are installed. This wizard will help you "
+ + "migrate your configuration and data to the new connectors.",
+ firstPage.getMessage());
+ assertTrue(firstPage.getControl() instanceof Composite);
+ Composite control = (Composite) firstPage.getControl();
+ assertEquals(1, control.getChildren().length);
+ assertTrue(control.getChildren()[0] instanceof Link);
+ }
+
+ @Test
+ public void secondPage() {
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ IWizardPage secondPage = firstPage.getNextPage();
+ assertEquals("Select Connectors", secondPage.getTitle());
+ assertEquals(
+ "Select the connectors to migrate. Your task list and repositories will be backed up before migration; you can "
+ + "undo the migration by selecting \"Restore Tasks from History\" in the Task List view "
+ + "menu and choosing the "
+ + "connector-migration-*.zip file stored in <workspace>/.metadata/.mylyn/backup.",
+ secondPage.getDescription());
+ assertTrue(secondPage.getControl() instanceof Composite);
+ Composite control = (Composite) secondPage.getControl();
+ assertEquals(1, control.getChildren().length);
+ assertTrue(control.getChildren()[0] instanceof Tree);
+ }
+
+ @Test
+ public void performFinishAfterConnectorsSelected()
+ throws InvocationTargetException, InterruptedException, IOException {
+ createMigrator(ImmutableMap.of("foo", "foo.new", "bar", "bar.new", "baz", "baz.new"));
+ IWizardContainer container = createWizard(new TestConnectorMigrationWizard(migrator));
+ spinEventLoop();
+ wizard.performFinish();
+ verify(container).run(eq(true), eq(true), any(IRunnableWithProgress.class));
+ verify(migrator).setConnectorsToMigrate(eq(ImmutableList.of("foo", "bar")));
+ verify(migrator).migrateConnectors(any(IProgressMonitor.class));
+ }
+
+ protected void createMigrator(Map<String, String> connectors) {
+ TaskRepositoryManager manager = spy(new TaskRepositoryManager());
+ createMigrator(connectors, manager);
+ }
+
+ private void createMigrator(Map<String, String> connectors, TaskRepositoryManager manager) {
+ DefaultTasksState tasksState = spy(new DefaultTasksState());
+ when(tasksState.getRepositoryManager()).thenReturn(manager);
+ migrator = spy(new ConnectorMigrator(connectors, "", tasksState, migrationUi));
+ }
+
+ @Test
+ public void performFinishNoConnectorsSelectedByDefault()
+ throws InvocationTargetException, InterruptedException, IOException {
+ createMigrator(ImmutableMap.of("foo", "foo.new", "bar", "bar.new", "baz", "baz.new"));
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ wizard.performFinish();
+ verify(container).run(eq(true), eq(true), any(IRunnableWithProgress.class));
+ verify(migrator).setConnectorsToMigrate(eq(ImmutableList.<String> of()));
+ verify(migrator).migrateConnectors(any(IProgressMonitor.class));
+ }
+
+ @Test
+ public void performFinishSelectsRelevantConnectors()
+ throws InvocationTargetException, InterruptedException, IOException {
+ TaskRepositoryManager manager = spy(new TaskRepositoryManager());
+
+ createAndAddConnector(manager, "mock", "Mock Connector");
+ createAndAddConnector(manager, "foo", "Foo Connector");
+ createAndAddConnector(manager, "bar", "Bar Connector");
+
+ manager.addRepository(new TaskRepository("mock", "http://mock"));
+ manager.addRepository(new TaskRepository("bar", "http://bar"));
+
+ createMigrator(ImmutableMap.of("foo", "foo.new", "bar", "bar.new", "mock", "mock.new"), manager);
+
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ wizard.performFinish();
+ verify(container).run(eq(true), eq(true), any(IRunnableWithProgress.class));
+ verify(migrator).setConnectorsToMigrate(eq(ImmutableList.of("bar", "mock")));
+ verify(migrator).migrateConnectors(any(IProgressMonitor.class));
+ }
+
+ private void createAndAddConnector(TaskRepositoryManager manager, String kind, String label) {
+ AbstractRepositoryConnector mockConnector = mock(AbstractRepositoryConnector.class);
+ when(mockConnector.getLabel()).thenReturn(label);
+ when(manager.getRepositoryConnector(kind)).thenReturn(mockConnector);
+ }
+
+ @Test
+ public void performFinishSetsErrorMessage() throws InvocationTargetException, InterruptedException, IOException {
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ doThrow(new InvocationTargetException(new IOException("Backup failed"))).when(container).run(any(Boolean.class),
+ any(Boolean.class), any(IRunnableWithProgress.class));
+ wizard.performFinish();
+ assertEquals("Backup failed", container.getCurrentPage().getErrorMessage());
+ }
+
+ @Test
+ public void isPageComplete() throws Exception {
+ IWizardContainer container = createWizard(new ConnectorMigrationWizard(migrator));
+ IWizardPage firstPage = container.getCurrentPage();
+ IWizardPage secondPage = firstPage.getNextPage();
+ assertTrue(firstPage.isPageComplete());
+ assertFalse(secondPage.isPageComplete());
+
+ container.showPage(secondPage);
+ assertTrue(firstPage.isPageComplete());
+ assertTrue(secondPage.isPageComplete());
+ }
+
+ @Test
+ public void createConnectorList() throws Exception {
+ CheckboxTreeViewer viewer = new ConnectorMigrationWizard(migrator).createConnectorList(WorkbenchUtil.getShell(),
+ ImmutableList.of("mock"));
+ IRepositoryManager manager = migrator.getRepositoryManager();
+ assertEquals(ImmutableList.of("mock"), viewer.getInput());
+ assertTrue(viewer.getLabelProvider() instanceof LabelProvider);
+ assertEquals("mock", ((LabelProvider) viewer.getLabelProvider()).getText("mock"));
+
+ AbstractRepositoryConnector connector = mock(AbstractRepositoryConnector.class);
+ when(connector.getLabel()).thenReturn("My Connector");
+ when(manager.getRepositoryConnector("mock")).thenReturn(connector);
+ manager.addRepository(new TaskRepository("mock", "http://mock"));
+ manager.addRepository(new TaskRepository("mock", "http://mock2"));
+ assertEquals("My Connector (used by 2 repositories)",
+ ((LabelProvider) viewer.getLabelProvider()).getText("mock"));
+ }
+
+ private IWizardContainer createWizard(ConnectorMigrationWizard wiz) {
+ wizard = spy(wiz);
+ WizardDialog dialog = new WizardDialog(WorkbenchUtil.getShell(), wizard);
+ dialog.create();
+ IWizardContainer container = spy(wizard.getContainer());
+ when(wizard.getContainer()).thenReturn(container);
+ return container;
+ }
+
+ private void spinEventLoop() {
+ while (Display.getCurrent().readAndDispatch()) {
+ }
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigratorTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigratorTest.java
new file mode 100644
index 000000000..01851b881
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigratorTest.java
@@ -0,0 +1,607 @@
+/*******************************************************************************
+ * 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 org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+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.DateRange;
+import org.eclipse.mylyn.internal.tasks.core.IRepositoryConstants;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryModel;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
+import org.eclipse.mylyn.internal.tasks.core.TaskActivityManager;
+import org.eclipse.mylyn.internal.tasks.core.TaskCategory;
+import org.eclipse.mylyn.internal.tasks.core.TaskJobFactory;
+import org.eclipse.mylyn.internal.tasks.core.TaskList;
+import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryManager;
+import org.eclipse.mylyn.internal.tasks.core.TaskTask;
+import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
+import org.eclipse.mylyn.internal.tasks.ui.TaskListBackupManager;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
+import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
+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.context.AbstractTaskContextStore;
+import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
+import org.eclipse.mylyn.tasks.core.data.TaskData;
+import org.eclipse.mylyn.tests.util.TestFixture;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class ConnectorMigratorTest {
+ public class SpyTasksState extends DefaultTasksState {
+
+ TaskActivityManager taskActivityManager = spy(super.getTaskActivityManager());
+
+ TaskDataManager taskDataManager = spy(super.getTaskDataManager());
+
+ RepositoryModel repositoryModel = spy(super.getRepositoryModel());
+
+ TaskList taskList = spy(super.getTaskList());
+
+ AbstractTaskContextStore contextStore = spy(super.getContextStore());
+
+ TaskJobFactory taskJobFactory = spy(super.getTaskJobFactory());
+
+ TaskRepositoryManager repositoryManager = spy(super.getRepositoryManager());
+
+ @Override
+ public TaskActivityManager getTaskActivityManager() {
+ return taskActivityManager;
+ }
+
+ @Override
+ public TaskDataManager getTaskDataManager() {
+ return taskDataManager;
+ }
+
+ @Override
+ public RepositoryModel getRepositoryModel() {
+ return repositoryModel;
+ }
+
+ @Override
+ public TaskList getTaskList() {
+ return taskList;
+ }
+
+ @Override
+ public AbstractTaskContextStore getContextStore() {
+ return contextStore;
+ }
+
+ @Override
+ public TaskJobFactory getTaskJobFactory() {
+ return taskJobFactory;
+ }
+
+ @Override
+ public TaskRepositoryManager getRepositoryManager() {
+ return repositoryManager;
+ }
+
+ }
+
+ private final ImmutableMap<String, String> kinds = ImmutableMap.of("mock", "mock.new");
+
+ private final TaskRepository repository = new TaskRepository("mock", "http://mock");
+
+ private final ImmutableSet<TaskRepository> singleRepository = ImmutableSet.of(repository);
+
+ private TaskRepository migratedRepository = new TaskRepository("mock.new", "http://mock");
+
+ private final AuthenticationCredentials repoCreds = new AuthenticationCredentials("u1", "p1");
+
+ private final AuthenticationCredentials proxyCreds = new AuthenticationCredentials("u2", "p2");
+
+ private final AuthenticationCredentials httpCreds = new AuthenticationCredentials("u3", "p3");
+
+ private final AuthenticationCredentials certCreds = new AuthenticationCredentials("u4", "p4");
+
+ private final AbstractRepositoryConnector connector = mock(AbstractRepositoryConnector.class);
+
+ private final AbstractRepositoryConnector newConnector = mock(AbstractRepositoryConnector.class);
+
+ private final TaskListView taskList = mock(TaskListView.class);
+
+ private final TaskListBackupManager backupManager = mock(TaskListBackupManager.class);
+
+ DefaultTasksState tasksState = spy(new SpyTasksState());
+
+ private TaskRepositoryManager manager = tasksState.getRepositoryManager();
+
+ private final ConnectorMigrationUi migrationUi = spy(new ConnectorMigrationUi(taskList, backupManager, tasksState));
+
+ @SuppressWarnings("unchecked")
+ @Before
+ public void setUp() {
+ doNothing().when(migrationUi).warnOfValidationFailure((List<TaskRepository>) any(List.class));
+ doNothing().when(migrationUi).notifyMigrationComplete();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ TestFixture.resetTaskList();
+ new File("test-context.zip").delete();
+ }
+
+ @Test
+ public void setConnectorsToMigrate() throws Exception {
+ ConnectorMigrator migrator = createMigrator(true, true, ImmutableMap.of("mock", "mock.new", "kind", "kind.new"),
+ ImmutableSet.of(repository, new TaskRepository("kind", "http://mock")), true);
+ try {
+ migrator.setConnectorsToMigrate(ImmutableList.of("foo"));
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {// NOSONAR
+ }
+ migrator.setConnectorsToMigrate(ImmutableList.of("kind"));
+ assertEquals(ImmutableMap.of("kind", "kind.new"), migrator.getSelectedConnectors());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void migrateConnectors() throws Exception {
+ assertMigrateConnectors();
+ verify(migrationUi, never()).warnOfValidationFailure((List<TaskRepository>) any());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void migrateConnectorsValidationFailure() throws Exception {
+ when(newConnector.validateRepository(any(TaskRepository.class), any(IProgressMonitor.class)))
+ .thenThrow(CoreException.class);
+ assertMigrateConnectors();
+ verify(migrationUi).warnOfValidationFailure(ImmutableList.of(migratedRepository));
+ }
+
+ @Test
+ public void cancelMigration() throws Exception {
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, singleRepository, true));
+ IProgressMonitor monitor = new NullProgressMonitor();
+ monitor.setCanceled(true);
+ IProgressMonitor spyMonitor = spy(monitor);
+ try {
+ migrator.setConnectorsToMigrate(ImmutableList.of("mock"));
+ migrator.migrateConnectors(spyMonitor);
+ fail("Expected OperationCanceledException");
+ } catch (OperationCanceledException e) {// NOSONAR
+ }
+ verify(spyMonitor).beginTask("Migrating repositories", 2);
+ verify(spyMonitor).isCanceled();
+ verify(manager, never()).getRepositoryConnector(any(String.class));
+ verify(manager, never()).addRepository(any(TaskRepository.class));
+ verify(migrator, never()).getMigratedRepository(any(String.class), any(TaskRepository.class));
+ }
+
+ private ConnectorMigrator assertMigrateConnectors() throws CoreException, IOException {
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, singleRepository, true));
+ IProgressMonitor monitor = migrateConnectors(migrator);
+
+ assertTrue(repository.isOffline());
+ assertEquals("http://mock (Unsupported, do not delete)", repository.getRepositoryLabel());
+ InOrder inOrder = inOrder(manager, newConnector, migrationUi, migrator, monitor);
+ inOrder.verify(monitor).beginTask("Migrating repositories", 2);
+
+ inOrder.verify(migrationUi).backupTaskList(monitor);
+ inOrder.verify(monitor).subTask("Backing up task list");
+ inOrder.verify(monitor).worked(1);
+
+ inOrder.verify(monitor).subTask("Migrating http://mock");
+ inOrder.verify(migrator).migrateRepository("mock.new", "http://mock", repository);
+ inOrder.verify(manager).addRepository(migratedRepository);
+ inOrder.verify(migrator).disconnect(repository);
+ inOrder.verify(monitor).worked(1);
+ inOrder.verify(monitor).beginTask("Validating repository connections", 1);
+ inOrder.verify(monitor).subTask("Validating connection to http://mock");
+ inOrder.verify(newConnector).validateRepository(migratedRepository, monitor);
+ inOrder.verify(monitor).done();
+ verifyNoMoreInteractions(newConnector);
+ return migrator;
+ }
+
+ private IProgressMonitor migrateConnectors(ConnectorMigrator migrator) throws IOException {
+ IProgressMonitor monitor = mock(IProgressMonitor.class);
+ migrator.setConnectorsToMigrate(ImmutableList.of("mock"));
+ migrator.migrateConnectors(monitor);
+ return monitor;
+ }
+
+ @Test
+ public void migrateRepository() throws Exception {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, singleRepository, true);
+ populateRepository();
+
+ migratedRepository = migrator.getMigratedRepository("mock.new", repository);
+ assertEquals("foovalue", migratedRepository.getProperty("foo"));
+ assertEquals("barvalue", migratedRepository.getProperty("bar"));
+ assertNull(migratedRepository.getProperty(IRepositoryConstants.PROPERTY_SYNCTIMESTAMP));
+ assertEquals("My Label", migratedRepository.getRepositoryLabel());
+ assertEquals("mock.new", migratedRepository.getConnectorKind());
+
+ assertEquals(repoCreds, migratedRepository.getCredentials(AuthenticationType.REPOSITORY));
+ assertEquals(proxyCreds, migratedRepository.getCredentials(AuthenticationType.PROXY));
+ assertEquals(httpCreds, migratedRepository.getCredentials(AuthenticationType.HTTP));
+ assertEquals(certCreds, migratedRepository.getCredentials(AuthenticationType.CERTIFICATE));
+
+ assertTrue(migratedRepository.getSavePassword(AuthenticationType.REPOSITORY));
+ assertTrue(migratedRepository.getSavePassword(AuthenticationType.PROXY));
+ assertFalse(migratedRepository.getSavePassword(AuthenticationType.HTTP));
+ assertFalse(migratedRepository.getSavePassword(AuthenticationType.CERTIFICATE));
+ }
+
+ @Test
+ public void migrateExistingRepositoryDoesNothing() throws Exception {
+ migratedRepository.setRepositoryLabel("My Old Label");
+
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, singleRepository, true);
+ when(manager.getRepository("mock.new", "http://mock")).thenReturn(migratedRepository);
+ populateRepository();
+
+ assertSame(migratedRepository, migrator.getMigratedRepository("mock.new", repository));
+ assertNull(migratedRepository.getProperty("foo"));
+ assertNull(migratedRepository.getProperty("bar"));
+ assertNull(migratedRepository.getProperty(IRepositoryConstants.PROPERTY_SYNCTIMESTAMP));
+ assertEquals("My Old Label", migratedRepository.getRepositoryLabel());
+ assertEquals("mock.new", migratedRepository.getConnectorKind());
+
+ assertNull(migratedRepository.getCredentials(AuthenticationType.REPOSITORY));
+ assertNull(migratedRepository.getCredentials(AuthenticationType.PROXY));
+ assertNull(migratedRepository.getCredentials(AuthenticationType.HTTP));
+ assertNull(migratedRepository.getCredentials(AuthenticationType.CERTIFICATE));
+
+ }
+
+ private void populateRepository() {
+ repository.setProperty("foo", "foovalue");
+ repository.setProperty("bar", "barvalue");
+ repository.setProperty(IRepositoryConstants.PROPERTY_SYNCTIMESTAMP, "123");
+ repository.setRepositoryLabel("My Label");
+ repository.setCredentials(AuthenticationType.REPOSITORY, repoCreds, true);
+ repository.setCredentials(AuthenticationType.PROXY, proxyCreds, true);
+ repository.setCredentials(AuthenticationType.HTTP, httpCreds, false);
+ repository.setCredentials(AuthenticationType.CERTIFICATE, certCreds, false);
+ }
+
+ @Test
+ public void needsMigrationEmptyKinds() {
+ try {
+ createMigrator(true, true, ImmutableMap.<String, String> of(), singleRepository, true);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {// NOSONAR
+ }
+ }
+
+ @Test
+ public void needsMigrationNoRepositories() {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, ImmutableSet.<TaskRepository> of(), true);
+ assertFalse(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationNoConnectors() {
+ ConnectorMigrator migrator = createMigrator(false, false, kinds, singleRepository, true);
+ assertFalse(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationNoNewConnector() {
+ ConnectorMigrator migrator = createMigrator(true, false, kinds, singleRepository, true);
+ assertFalse(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationNoOldConnector() {
+ ConnectorMigrator migrator = createMigrator(false, true, kinds, singleRepository, true);
+ assertFalse(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationOneRepository() {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, singleRepository, true);
+ assertTrue(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationMultipleRepositories() {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds,
+ ImmutableSet.of(repository, new TaskRepository("mock", "http://mock2")), true);
+ assertTrue(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationMultiKindsOneRepository() {
+ ImmutableMap<String, String> multiKinds = ImmutableMap.of("mock", "mock.new", "kind", "kind.new");
+ ConnectorMigrator migrator = createMigrator(true, true, multiKinds, singleRepository, true);
+ assertTrue(migrator.needsMigration());
+
+ migrator = createMigrator(true, true, multiKinds, ImmutableSet.of(new TaskRepository("kind", "http://mock")),
+ true);
+ assertTrue(migrator.needsMigration());
+ }
+
+ @Test
+ public void needsMigrationMultiKindsMultiRepositories() {
+ ConnectorMigrator migrator = createMigrator(true, true, ImmutableMap.of("mock", "mock.new", "kind", "kind.new"),
+ ImmutableSet.of(repository, new TaskRepository("kind", "http://mock")), true);
+ assertTrue(migrator.needsMigration());
+ }
+
+ @Test
+ public void migrateTasksWaitsForSyncJobs() throws Exception {
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, ImmutableSet.of(repository), false));
+ JobListener listener = mock(JobListener.class);
+ when(listener.isComplete()).thenReturn(false, false, true);
+ when(migrator.getSyncTaskJobListener()).thenReturn(listener);
+ migrator.migrateTasks(new NullProgressMonitor());
+ verify(listener, times(3)).isComplete();
+ }
+
+ @Test
+ public void migrateTasks() throws Exception {
+ when(newConnector.getConnectorKind()).thenReturn("mock.new");
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, ImmutableSet.of(repository), false));
+ TaskData taskData2 = new TaskData(mock(TaskAttributeMapper.class), "mock.new", repository.getRepositoryUrl(),
+ "2.migrated");
+ when(migrator.getTaskData(eq("key2"), eq(newConnector), any(TaskRepository.class), any(IProgressMonitor.class)))
+ .thenReturn(taskData2);
+ ITask task1 = new TaskTask("mock", "http://mock", "1");
+ task1.setTaskKey("key1");
+ ((AbstractTask) task1).setSynchronizationState(SynchronizationState.INCOMING_NEW);
+ ITask task2 = new TaskTask("mock", "http://mock", "2");
+ task2.setTaskKey("key2");
+ ((AbstractTask) task2).setSynchronizationState(SynchronizationState.INCOMING);
+ ITask task1Migrated = new TaskTask("mock.new", "http://mock", "1.migrated");
+ task1Migrated.setTaskKey("key1");
+ TaskTask taskOtherRepo = new TaskTask("mock", "http://other-mock", "1");
+ tasksState.getTaskList().addTask(task1);
+ tasksState.getTaskList().addTask(task1Migrated);
+ tasksState.getTaskList().addTask(taskOtherRepo);
+ tasksState.getTaskList().addTask(task2);
+ RepositoryQuery query = new RepositoryQuery("mock", "mock");
+ query.setRepositoryUrl("http://mock");
+ tasksState.getTaskList().addQuery(query);
+ migrateConnectors(migrator);
+ NullProgressMonitor monitor = new NullProgressMonitor();
+
+ migrator.migrateTasks(monitor);
+ verify(tasksState.getTaskActivityManager()).deactivateActiveTask();
+ TaskRepository newRepository = manager.getRepository("mock.new", "http://mock");
+ verify(migrator).migrateTasks(ImmutableSet.of(task1, task2), repository, newRepository,
+ manager.getRepositoryConnector("mock.new"), monitor);
+
+ verify(migrator, never()).getTaskData("key1", newConnector, newRepository, monitor);
+ verify(migrator, never()).createTask(argThat(not(taskData2)), any(TaskRepository.class));
+ verify(migrator).getTaskData("key2", newConnector, newRepository, monitor);
+ verify(migrator).createTask(taskData2, newRepository);
+ verify(migrator).migratePrivateData((AbstractTask) task1, (AbstractTask) task1Migrated, monitor);
+ ITask task2Migrated = new TaskTask("mock.new", "http://mock", "2.migrated");
+ task2Migrated.setTaskKey("key2");
+ verify(migrator).migratePrivateData((AbstractTask) task2, (AbstractTask) task2Migrated, monitor);
+
+ verify(migrationUi).delete(ImmutableSet.of(task1, task2), repository, newRepository, monitor);
+ assertEquals(SynchronizationState.INCOMING_NEW, tasksState.getTaskList()
+ .getTask(repository.getRepositoryUrl(), "1.migrated")
+ .getSynchronizationState());
+ assertEquals(SynchronizationState.INCOMING, tasksState.getTaskList()
+ .getTask(repository.getRepositoryUrl(), "2.migrated")
+ .getSynchronizationState());
+
+ assertEquals(ImmutableSet.of(taskOtherRepo, task1Migrated, task2Migrated),
+ ImmutableSet.copyOf(tasksState.getTaskList().getAllTasks()));
+ verify(tasksState.getRepositoryManager()).removeRepository(repository);
+ assertTrue(tasksState.getTaskList().getQueries().isEmpty());
+ assertEquals(ImmutableSet.of(newRepository), tasksState.getRepositoryManager().getRepositories("mock.new"));
+ }
+
+ @Test
+ public void migrateTasksSameId() throws Exception {
+ when(newConnector.getConnectorKind()).thenReturn("mock.new");
+ ConnectorMigrator migrator = spy(createMigrator(true, true, kinds, ImmutableSet.of(repository), false));
+ TaskData taskData1 = new TaskData(mock(TaskAttributeMapper.class), "mock.new", repository.getRepositoryUrl(),
+ "1");
+ when(migrator.getTaskData(eq("key1"), eq(newConnector), any(TaskRepository.class), any(IProgressMonitor.class)))
+ .thenReturn(taskData1);
+ TaskData taskData2 = new TaskData(mock(TaskAttributeMapper.class), "mock.new", repository.getRepositoryUrl(),
+ "2");
+ when(migrator.getTaskData(eq("key2"), eq(newConnector), any(TaskRepository.class), any(IProgressMonitor.class)))
+ .thenReturn(taskData2);
+ ITask task1 = new TaskTask("mock", "http://mock", "1");
+ task1.setTaskKey("key1");
+ ((AbstractTask) task1).setSynchronizationState(SynchronizationState.INCOMING_NEW);
+ ITask task2 = new TaskTask("mock", "http://mock", "2");
+ task2.setTaskKey("key2");
+ ((AbstractTask) task2).setSynchronizationState(SynchronizationState.INCOMING);
+ ITask task1Migrated = new TaskTask("mock.new", "http://mock", "1");
+ task1Migrated.setTaskKey("key1");
+ TaskTask taskOtherRepo = new TaskTask("mock", "http://other-mock", "1");
+ tasksState.getTaskList().addTask(task1);
+ tasksState.getTaskList().addTask(task1Migrated);
+ tasksState.getTaskList().addTask(taskOtherRepo);
+ tasksState.getTaskList().addTask(task2);
+ RepositoryQuery query = new RepositoryQuery("mock", "mock");
+ query.setRepositoryUrl("http://mock");
+ tasksState.getTaskList().addQuery(query);
+ migrateConnectors(migrator);
+ NullProgressMonitor monitor = new NullProgressMonitor();
+
+ migrator.migrateTasks(monitor);
+ verify(tasksState.getTaskActivityManager()).deactivateActiveTask();
+ TaskRepository newRepository = manager.getRepository("mock.new", "http://mock");
+ verify(migrator).migrateTasks(ImmutableSet.of(task1, task2), repository, newRepository,
+ manager.getRepositoryConnector("mock.new"), monitor);
+
+ verify(migrator).getTaskData("key1", newConnector, newRepository, monitor);
+ verify(migrator).getTaskData("key2", newConnector, newRepository, monitor);
+ verify(migrator).createTask(taskData2, newRepository);
+ ITask task2Migrated = new TaskTask("mock.new", "http://mock", "2");
+ task2Migrated.setTaskKey("key2");
+ verify(migrator).migratePrivateData((AbstractTask) task2, (AbstractTask) task2Migrated, monitor);
+ verify(migrator).migratePrivateData((AbstractTask) task1, (AbstractTask) task1Migrated, monitor);
+
+ verify(migrationUi).delete(ImmutableSet.of(task1, task2), repository, newRepository, monitor);
+ assertEquals(SynchronizationState.INCOMING_NEW,
+ tasksState.getTaskList().getTask(repository.getRepositoryUrl(), "1").getSynchronizationState());
+ assertEquals(SynchronizationState.INCOMING,
+ tasksState.getTaskList().getTask(repository.getRepositoryUrl(), "2").getSynchronizationState());
+
+ assertEquals(ImmutableSet.of(taskOtherRepo, task1Migrated, task2Migrated),
+ ImmutableSet.copyOf(tasksState.getTaskList().getAllTasks()));
+ verify(tasksState.getRepositoryManager()).removeRepository(repository);
+ assertTrue(tasksState.getTaskList().getQueries().isEmpty());
+ assertEquals(ImmutableSet.of(newRepository), tasksState.getRepositoryManager().getRepositories("mock.new"));
+ }
+
+ @Test
+ public void migratePrivateData() throws Exception {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, ImmutableSet.of(repository), false);
+ AbstractTask oldTask = new TaskTask("mock", "http://mock", "1");
+ AbstractTask newTask = new TaskTask("mock.new", "http://mock", "1.migrated");
+
+ oldTask.setNotes("some notes");
+ DateRange scheduledDate = new DateRange(createCalendar(3));
+ Calendar dueDate = createCalendar(5);
+ tasksState.getTaskActivityManager().setScheduledFor(oldTask, scheduledDate);
+ tasksState.getTaskActivityManager().setDueDate(oldTask, dueDate.getTime());
+ oldTask.setEstimatedTimeHours(7);
+
+ migrator.migratePrivateData(oldTask, newTask, new NullProgressMonitor());
+ assertEquals("some notes", newTask.getNotes());
+ assertEquals(scheduledDate, newTask.getScheduledForDate());
+ assertEquals(dueDate.getTime(), newTask.getDueDate());
+ assertEquals(7, newTask.getEstimatedTimeHours());
+ }
+
+ @Test
+ public void migrateCategories() throws Exception {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, ImmutableSet.of(repository), false);
+ AbstractTask oldTask1 = new TaskTask("mock", "http://mock", "1");
+ AbstractTask oldTask2 = new TaskTask("mock", "http://mock", "2");
+ AbstractTask newTask1 = new TaskTask("mock.new", "http://mock", "1.migrated");
+ AbstractTask newTask2 = new TaskTask("mock.new", "http://mock", "2.migrated");
+
+ TaskCategory category1 = new TaskCategory("category1");
+ TaskCategory category2 = new TaskCategory("category2");
+ tasksState.getTaskList().addCategory(category1);
+ tasksState.getTaskList().addCategory(category2);
+ tasksState.getTaskList().addTask(oldTask1, category1);
+ tasksState.getTaskList().addTask(oldTask2, category2);
+
+ migrator.migratePrivateData(oldTask1, newTask1, new NullProgressMonitor());
+ migrator.migratePrivateData(oldTask2, newTask2, new NullProgressMonitor());
+ assertEquals(category1, getCategory(newTask1));
+ assertEquals(category2, getCategory(newTask2));
+ }
+
+ @Test
+ public void getCategories() throws Exception {
+ ConnectorMigrator migrator = createMigrator(true, true, kinds, ImmutableSet.of(repository), false);
+ AbstractTask oldTask1 = new TaskTask("mock", "http://mock", "1");
+ AbstractTask oldTask2 = new TaskTask("mock", "http://mock", "2");
+ AbstractTask newTask1 = new TaskTask("mock.new", "http://mock", "1.migrated");
+ AbstractTask newTask2 = new TaskTask("mock.new", "http://mock", "2.migrated");
+
+ TaskCategory category1 = new TaskCategory("category1");
+ TaskCategory category2 = new TaskCategory("category2");
+ tasksState.getTaskList().addCategory(category1);
+ tasksState.getTaskList().addCategory(category2);
+ tasksState.getTaskList().addTask(oldTask1, category1);
+ tasksState.getTaskList().addTask(newTask1, category1);
+ tasksState.getTaskList().addTask(oldTask2, category2);
+ tasksState.getTaskList().addTask(newTask2, category2);
+ tasksState.getTaskList().addTask(new TaskTask("mock.new", "http://mock", "not categorized"));
+
+ ImmutableMap<AbstractTask, TaskCategory> expected = ImmutableMap.of(oldTask1, category1, oldTask2, category2,
+ newTask1, category1, newTask2, category2);
+ assertEquals(expected, migrator.getCategories());
+
+ tasksState.getTaskList().addTask(new TaskTask("mock.new", "http://mock", "3"), category1);
+ assertEquals(expected, migrator.getCategories());
+ }
+
+ private AbstractTaskCategory getCategory(AbstractTask newTask) {
+ for (AbstractTaskCategory category : tasksState.getTaskList().getCategories()) {
+ Optional<ITask> task = Iterables.tryFind(category.getChildren(), Predicates.<ITask> equalTo(newTask));
+ if (task.isPresent()) {
+ return category;
+ }
+ }
+ return null;
+ }
+
+ private Calendar createCalendar(int daysInFuture) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, daysInFuture);
+ return cal;
+ }
+
+ private ConnectorMigrator createMigrator(boolean hasOldConnector, boolean hasNewConnector,
+ Map<String, String> kinds, Set<TaskRepository> repositories, boolean mockManager) {
+ if (mockManager) {
+ manager = mock(TaskRepositoryManager.class);
+ }
+ when(tasksState.getRepositoryManager()).thenReturn(manager);
+ ConnectorMigrator migrator = new ConnectorMigrator(kinds, "", tasksState, migrationUi);
+ when(manager.getRepositories("mock")).thenReturn(repositories);
+ if (hasOldConnector) {
+ when(manager.getRepositoryConnector("mock")).thenReturn(connector);
+ when(manager.getRepositoryConnector("kind")).thenReturn(connector);
+ }
+ if (hasNewConnector) {
+ when(manager.getRepositoryConnector("mock.new")).thenReturn(newConnector);
+ when(manager.getRepositoryConnector("kind.new")).thenReturn(newConnector);
+ }
+ return migrator;
+ }
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/META-INF/MANIFEST.MF b/org.eclipse.mylyn.tasks.ui/META-INF/MANIFEST.MF
index 377f277c6..0e1d7e200 100644
--- a/org.eclipse.mylyn.tasks.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.mylyn.tasks.ui/META-INF/MANIFEST.MF
@@ -44,6 +44,7 @@ Export-Package: org.eclipse.mylyn.internal.provisional.tasks.ui.wizards;x-intern
org.eclipse.mylyn.internal.tasks.ui.dialogs;x-internal:=true,
org.eclipse.mylyn.internal.tasks.ui.editors;x-internal:=true,
org.eclipse.mylyn.internal.tasks.ui.editors.outline;x-internal:=true,
+ org.eclipse.mylyn.internal.tasks.ui.migrator;x-internal:=true,
org.eclipse.mylyn.internal.tasks.ui.notifications;x-internal:=true,
org.eclipse.mylyn.internal.tasks.ui.preferences;x-internal:=true,
org.eclipse.mylyn.internal.tasks.ui.properties;x-internal:=true,
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizard.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizard.java
new file mode 100644
index 000000000..a7ef2cd7b
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/CompleteConnectorMigrationWizard.java
@@ -0,0 +1,254 @@
+/*******************************************************************************
+ * 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.collect.Iterables.any;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+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.isSynchronizing;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
+import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
+import org.eclipse.mylyn.internal.tasks.ui.util.TasksUiInternal;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskRepositoryLabelProvider;
+import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.ui.AbstractRepositoryConnectorUi;
+import org.eclipse.mylyn.tasks.ui.TaskElementLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.PlatformUI;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+public class CompleteConnectorMigrationWizard extends Wizard {
+ final class MapContentProvider implements ITreeContentProvider {
+ private final Map<?, ? extends Collection<?>> map;
+
+ private MapContentProvider(Map<?, ? extends Collection<?>> map) {
+ this.map = map;
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ return map.containsKey(element);
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ return null;
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Map) {
+ return ((Map<?, ?>) inputElement).keySet().toArray();
+ }
+ return null;
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (map.get(parentElement) == null) {
+ return null;
+ }
+ return map.get(parentElement).toArray();
+ }
+ }
+
+ private final ConnectorMigrator migrator;
+
+ public CompleteConnectorMigrationWizard(ConnectorMigrator migrator) {
+ this.migrator = migrator;
+ }
+
+ @Override
+ public void addPages() {
+ setWindowTitle(Messages.CompleteConnectorMigrationWizard_Complete_Connector_Migration);
+ addPage(new WizardPage(Messages.CompleteConnectorMigrationWizard_Migrate_Queries) {
+
+ @Override
+ public void createControl(Composite parent) {
+ setTitle(Messages.CompleteConnectorMigrationWizard_Have_You_Recreated_Your_Queries);
+ setMessage(Messages.CompleteConnectorMigrationWizard_first_page_message, IMessageProvider.INFORMATION);
+ Composite c = new Composite(parent, SWT.NONE);
+ GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(true).applyTo(c);
+ Label oldQueriesLabel = new Label(c, SWT.NONE);
+ oldQueriesLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+ oldQueriesLabel.setText(Messages.CompleteConnectorMigrationWizard_Queries_Using_Old_Connectors);
+ Label newQueriesLabel = new Label(c, SWT.NONE);
+ newQueriesLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
+ newQueriesLabel.setText(Messages.CompleteConnectorMigrationWizard_Queries_Using_New_Connectors);
+ createQueryTree(c, createRepositoryQueryMap(migrator.getSelectedConnectors().keySet()));
+ createQueryTree(c, createRepositoryQueryMap(migrator.getSelectedConnectors().values()));
+ setControl(c);
+ }
+
+ });
+ addPage(new WizardPage(Messages.CompleteConnectorMigrationWizard_Complete_Migration) {
+
+ @Override
+ public void createControl(Composite parent) {
+ setTitle(Messages.CompleteConnectorMigrationWizard_Complete_Migration);
+ setMessage(Messages.CompleteConnectorMigrationWizard_second_page_message, IMessageProvider.INFORMATION);
+ Composite c = new Composite(parent, SWT.NONE);
+ GridLayoutFactory.fillDefaults().applyTo(c);
+ Text text = new Text(c, SWT.READ_ONLY | SWT.MULTI | SWT.WRAP);
+ text.setText(Messages.CompleteConnectorMigrationWizard_second_page_text);
+ GridDataFactory.fillDefaults()
+ .align(SWT.FILL, SWT.FILL)
+ .grab(true, true)
+ .hint(600, SWT.DEFAULT)
+ .applyTo(text);
+ setControl(c);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ return super.isPageComplete() && isCurrentPage();
+ }
+ });
+ }
+
+ protected TreeViewer createQueryTree(Composite parent,
+ final Map<TaskRepository, ? extends Set<RepositoryQuery>> queries) {
+ final TreeViewer viewer = new TreeViewer(parent);
+ GridDataFactory.fillDefaults().grab(false, true).hint(500, SWT.DEFAULT).applyTo(viewer.getControl());
+ viewer.setContentProvider(new MapContentProvider(queries));
+ viewer.setInput(queries);
+ viewer.setLabelProvider(new TaskElementLabelProvider() {
+ private final TaskRepositoryLabelProvider repositoryLabelProvider = new TaskRepositoryLabelProvider();
+
+ @Override
+ public Image getImage(Object element) {
+ if (element instanceof TaskRepository) {
+ return repositoryLabelProvider.getImage(element);
+ }
+ return super.getImage(element);
+ }
+
+ @Override
+ public String getText(Object object) {
+ if (object instanceof TaskRepository) {
+ return repositoryLabelProvider.getText(object);
+ }
+ return super.getText(object);
+ }
+ });
+ viewer.addDoubleClickListener(new IDoubleClickListener() {
+
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ if (viewer.getSelection() instanceof IStructuredSelection) {
+ Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
+ if (element instanceof IRepositoryQuery) {
+ IRepositoryQuery query = (IRepositoryQuery) element;
+ AbstractRepositoryConnectorUi connectorUi = TasksUiPlugin
+ .getConnectorUi(query.getConnectorKind());
+ TasksUiInternal.openEditQueryDialog(connectorUi, query);
+ }
+ }
+ }
+ });
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ viewer.expandAll();
+ }
+ });
+ return viewer;
+ }
+
+ @Override
+ public boolean performFinish() {
+ Job job = new Job("Migrating Tasks and Private Data") {
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ monitor.beginTask("Completing connector migration", IProgressMonitor.UNKNOWN);
+ Collection<String> newConnectors = migrator.getSelectedConnectors().values();
+ waitForQueriesToSynchronize(newConnectors, monitor);
+ migrator.migrateTasks(monitor);
+ return Status.OK_STATUS;
+ }
+
+ };
+ job.setUser(true);
+ job.setSystem(false);
+ job.schedule();
+ return true;
+ }
+
+ protected void waitForQueriesToSynchronize(Collection<String> newConnectors, IProgressMonitor monitor) {
+ monitor.subTask("Waiting for queries to complete synchronization");
+ Iterable<RepositoryQuery> queries = Iterables.concat(createRepositoryQueryMap(newConnectors).values());
+ long start = System.currentTimeMillis();
+ while (any(queries, isSynchronizing())
+ && System.currentTimeMillis() - start < MILLISECONDS.convert(20, MINUTES)) {
+ try {
+ Thread.sleep(MILLISECONDS.convert(3, SECONDS));
+ } catch (InterruptedException e) {// NOSONAR
+ }
+ }
+ }
+
+ protected Map<TaskRepository, Set<RepositoryQuery>> createRepositoryQueryMap(Collection<String> kinds) {
+ Builder<TaskRepository, Set<RepositoryQuery>> repositories = ImmutableMap.builder();
+ for (final String kind : kinds) {
+ for (TaskRepository repository : migrator.getRepositoryManager().getRepositories(kind)) {
+ Set<RepositoryQuery> queriesForUrl = TasksUiPlugin.getTaskList()
+ .getRepositoryQueries(repository.getRepositoryUrl());
+ Set<RepositoryQuery> queries = Sets.filter(queriesForUrl, isQueryForConnector(kind));
+ if (!queries.isEmpty()) {
+ repositories.put(repository, queries);
+ }
+ }
+ }
+ return repositories.build();
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUi.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUi.java
new file mode 100644
index 000000000..8c2edcc57
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationUi.java
@@ -0,0 +1,312 @@
+/*******************************************************************************
+ * 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 org.eclipse.mylyn.internal.tasks.ui.migrator.TaskPredicates.isQueryForRepository;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+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.commons.workbench.WorkbenchUtil;
+import org.eclipse.mylyn.internal.commons.notifications.feed.ServiceMessage;
+import org.eclipse.mylyn.internal.tasks.core.ITaskListRunnable;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
+import org.eclipse.mylyn.internal.tasks.core.UnsubmittedTaskContainer;
+import org.eclipse.mylyn.internal.tasks.ui.TaskListBackupManager;
+import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
+import org.eclipse.mylyn.internal.tasks.ui.actions.DeleteAction;
+import org.eclipse.mylyn.internal.tasks.ui.util.TaskDataSnapshotOperation;
+import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.ui.PlatformUI;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+public class ConnectorMigrationUi {
+
+ private static final String MIGRATE = "migrate"; //$NON-NLS-1$
+
+ private static final String COMLETE_MIGRATION = "complete-migration"; //$NON-NLS-1$
+
+ private final TaskListView taskListView;
+
+ private final TaskListBackupManager backupManager;
+
+ private final TasksState tasksState;
+
+ public ConnectorMigrationUi(TaskListView taskListView, TaskListBackupManager backupManager, TasksState tasksState) {
+ this.taskListView = taskListView;
+ this.backupManager = backupManager;
+ this.tasksState = tasksState;
+ }
+
+ /**
+ * @noextend This class is not intended to be subclassed by clients.
+ * @noinstantiate This class is not intended to be instantiated by clients.
+ */
+ protected class CompleteMigrationJob extends Job {
+ private boolean finishedCompleteMigrationWizard;
+
+ private final ConnectorMigrator migrator;
+
+ private CompleteMigrationJob(String name, ConnectorMigrator migrator) {
+ super(name);
+ this.migrator = migrator;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ if (!finishedCompleteMigrationWizard) {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (taskListView != null && taskListView.getServiceMessageControl() != null) {
+ ServiceMessage message = new ServiceMessage("") {
+
+ @Override
+ public boolean openLink(String link) {
+ if (link.equals(COMLETE_MIGRATION)) {
+ if (createCompleteMigrationWizard(migrator).open() == Window.OK) {
+ finishedCompleteMigrationWizard = true;
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ message.setTitle("Connector Migration");
+ message.setDescription(NLS.bind(Messages.ConnectorMigrator_complete_migration_prompt_title,
+ COMLETE_MIGRATION));
+ message.setImage(Dialog.DLG_IMG_MESSAGE_WARNING);
+ taskListView.getServiceMessageControl().setMessage(message);
+ }
+ }
+ });
+ schedule(TimeUnit.MILLISECONDS.convert(getCompletionPromptFrequency(), TimeUnit.SECONDS));
+ }
+ return Status.OK_STATUS;
+ }
+
+ public void dispose() {
+ finishedCompleteMigrationWizard = true;
+ }
+ }
+
+ public void promptToMigrate(final ConnectorMigrator migrator) {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (taskListView != null && taskListView.getServiceMessageControl() != null) {
+ ServiceMessage message = new ServiceMessage("") {
+ @Override
+ public boolean openLink(String link) {
+ if (link.equals(MIGRATE)) {
+ if (createMigrationWizard(migrator).open() == Window.OK) {
+ createPromptToCompleteMigrationJob(migrator).schedule();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ };
+ message.setTitle("End of Connector Support");
+ message.setDescription(
+ NLS.bind(Messages.ConnectorMigrator_complete_migration_prompt_message, MIGRATE));
+ message.setImage(Dialog.DLG_IMG_MESSAGE_INFO);
+ taskListView.getServiceMessageControl().setMessage(message);
+ }
+ }
+ });
+ }
+
+ protected Job createPromptToCompleteMigrationJob(ConnectorMigrator migrator) {
+ Job job = new CompleteMigrationJob("Complete Connector Migration Prompt", migrator);
+ job.setUser(false);
+ job.setSystem(true);
+ return job;
+ }
+
+ protected WizardDialog createMigrationWizard(ConnectorMigrator migrator) {
+ return createWizardDialog(new ConnectorMigrationWizard(migrator));
+ }
+
+ protected WizardDialog createCompleteMigrationWizard(ConnectorMigrator migrator) {
+ return createWizardDialog(new CompleteConnectorMigrationWizard(migrator));
+ }
+
+ protected WizardDialog createWizardDialog(Wizard wizard) {
+ WizardDialog dialog = new WizardDialog(WorkbenchUtil.getShell(), wizard);
+ dialog.create();
+ dialog.setBlockOnOpen(true);
+ return dialog;
+ }
+
+ /**
+ * @return the frequency in seconds with which the completion prompt will be shown
+ */
+ protected int getCompletionPromptFrequency() {
+ return 5;
+ }
+
+ public void warnOfValidationFailure(final List<TaskRepository> failedValidation) {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ String repositoryList = Joiner.on("\n") //$NON-NLS-1$
+ .join(Iterables.transform(failedValidation, repositoryToLabel()));
+ MessageDialog.openWarning(WorkbenchUtil.getShell(), "Validation Failed",
+ NLS.bind(Messages.ConnectorMigrationWizard_validation_failed, repositoryList));
+ }
+ });
+ }
+
+ private static Function<TaskRepository, String> repositoryToLabel() {
+ return new Function<TaskRepository, String>() {
+ @Override
+ public String apply(TaskRepository repository) {
+ return repository.getRepositoryLabel();
+ }
+ };
+ }
+
+ protected void backupTaskList(final IProgressMonitor monitor) throws IOException {
+ try {
+ monitor.subTask("Backing up task list");
+ String backupFolder = TasksUiPlugin.getDefault().getBackupFolderPath();
+ new File(backupFolder).mkdirs();
+ String fileName = getBackupFileName(new Date());
+ new TaskDataSnapshotOperation(backupFolder, fileName).run(new SubProgressMonitor(monitor, 1));
+ // also take a snapshot because user might try to restore from snapshot
+ PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ backupManager.backupNow(true);
+ }
+ });
+ monitor.worked(1);
+ } catch (InvocationTargetException e) {
+ throw (IOException) e.getCause();
+ }
+ }
+
+ protected String getBackupFileName(Date date) {
+ // we use an underscore in the date format to prevent TaskListBackupManager from thinking this is one of its backups
+ // and deleting it (TaskListBackupManager.MYLYN_BACKUP_REGEXP matches any string).
+ return "connector-migration-" + new SimpleDateFormat("yyyy_MM_dd_HHmmss").format(date) + ".zip"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ /**
+ * Deletes the given tasks and repository, and all queries associated with the repository, while preserving the
+ * credentials of <code>newRepository</code>.
+ *
+ * @param newRepository
+ */
+ protected void delete(final Set<ITask> tasks, final TaskRepository repository, final TaskRepository newRepository,
+ IProgressMonitor monitor) {
+ final Set<RepositoryQuery> queries = Sets.filter(tasksState.getTaskList().getQueries(),
+ isQueryForRepository(repository));
+ final UnsubmittedTaskContainer unsubmitted = tasksState.getTaskList()
+ .getUnsubmittedContainer(repository.getRepositoryUrl());
+ try {
+ monitor.subTask("Deleting old repository, tasks, and queries");
+ try {
+ DeleteAction.prepareDeletion(tasks);
+ if (unsubmitted != null) {
+ DeleteAction.prepareDeletion(unsubmitted.getChildren());
+ }
+ DeleteAction.prepareDeletion(queries);
+ } catch (Exception e) {// in case an error happens closing editors, we still want to delete
+ StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, e.getMessage(), e));
+ }
+ tasksState.getTaskList().run(new ITaskListRunnable() {
+ @Override
+ public void execute(IProgressMonitor monitor) throws CoreException {
+ for (ITask task : tasks) {
+ delete(task);
+ }
+ if (unsubmitted != null) {
+ for (ITask task : unsubmitted.getChildren()) {
+ delete(task);
+ }
+ }
+ DeleteAction.performDeletion(queries);
+ Map<AuthenticationType, AuthenticationCredentials> credentialsMap = new HashMap<>();
+ for (AuthenticationType type : AuthenticationType.values()) {
+ AuthenticationCredentials credentials = repository.getCredentials(type);
+ if (credentials != null) {
+ credentialsMap.put(type, credentials);
+ }
+ }
+ tasksState.getRepositoryManager().removeRepository(repository);
+ for (AuthenticationType type : credentialsMap.keySet()) {
+ newRepository.setCredentials(type, credentialsMap.get(type),
+ newRepository.getSavePassword(type));
+ }
+ }
+ }, monitor);
+ tasksState.getRepositoryModel().clear();
+ } catch (CoreException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Error deleting task", e));
+ }
+ }
+
+ /**
+ * Delete a task without deleting the context.
+ */
+ protected void delete(ITask task) {
+ tasksState.getTaskList().deleteTask(task);
+ try {
+ tasksState.getTaskDataManager().deleteTaskData(task);
+ } catch (CoreException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Failed to delete task data", e));//$NON-NLS-1$
+ }
+ }
+
+ public void notifyMigrationComplete() {
+ PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ MessageDialog.openInformation(WorkbenchUtil.getShell(), "Connector Migration Complete",
+ "Connector migration completed successfully. You may resume using the task list.");
+ }
+ });
+ }
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizard.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizard.java
new file mode 100644
index 000000000..762a2a7b0
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrationWizard.java
@@ -0,0 +1,216 @@
+/*******************************************************************************
+ * 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 java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.mylyn.commons.core.StatusHandler;
+import org.eclipse.mylyn.commons.workbench.browser.BrowserUtil;
+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.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+
+public class ConnectorMigrationWizard extends Wizard {
+ private static class CollectionContentProvider implements ITreeContentProvider {
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ return new Object[0];
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ return false;
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof Collection<?>) {
+ return ((Collection<?>) inputElement).toArray();
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+ }
+
+ private Object[] selectedConnectors = new Object[0];
+
+ private final ConnectorMigrator migrator;
+
+ public ConnectorMigrationWizard(ConnectorMigrator migrator) {
+ this.migrator = migrator;
+ setNeedsProgressMonitor(true);
+ }
+
+ @Override
+ public void addPages() {
+ setWindowTitle(Messages.ConnectorMigrationWizard_Connector_Migration);
+ addPage(new WizardPage(Messages.ConnectorMigrationWizard_End_of_Connector_Support) {
+
+ @Override
+ public void createControl(Composite parent) {
+ setTitle(Messages.ConnectorMigrationWizard_End_of_Connector_Support);
+ setMessage(Messages.ConnectorMigrationWizard_Message, IMessageProvider.INFORMATION);
+ Composite c = new Composite(parent, SWT.NONE);
+ GridLayoutFactory.fillDefaults().applyTo(c);
+ Link text = new Link(c, SWT.READ_ONLY | SWT.MULTI | SWT.WRAP);
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ BrowserUtil.openUrl(e.text, IWorkbenchBrowserSupport.AS_EXTERNAL);
+ }
+ });
+ text.setText(NLS.bind(Messages.ConnectorMigrationWizard_Body, migrator.getExplanatoryText()));
+ GridDataFactory.fillDefaults()
+ .align(SWT.FILL, SWT.FILL)
+ .grab(true, true)
+ .hint(600, SWT.DEFAULT)
+ .applyTo(text);
+ setControl(c);
+ }
+ });
+ addPage(new WizardPage(Messages.ConnectorMigrationWizard_Select_Connectors) {
+
+ @Override
+ public void createControl(Composite parent) {
+ setTitle(Messages.ConnectorMigrationWizard_Select_Connectors);
+ setDescription(Messages.ConnectorMigrationWizard_Select_the_connectors_to_migrate);
+ Composite c = new Composite(parent, SWT.NONE);
+ GridLayoutFactory.fillDefaults().applyTo(c);
+ List<String> kinds = getRelevantConnectorKinds(migrator.getConnectorKinds().keySet());
+ final CheckboxTreeViewer viewer = createConnectorList(c, kinds);
+ selectedConnectors = kinds.toArray();
+ viewer.setCheckedElements(selectedConnectors);
+ viewer.addCheckStateListener(new ICheckStateListener() {
+
+ @Override
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ selectedConnectors = viewer.getCheckedElements();
+ setPageComplete(selectedConnectors.length > 0);
+ }
+ });
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(viewer.getControl());
+ setControl(c);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ return super.isPageComplete() && isCurrentPage();
+ }
+
+ });
+ }
+
+ protected CheckboxTreeViewer createConnectorList(Composite parent, List<String> kinds) {
+ final CheckboxTreeViewer viewer = new CheckboxTreeViewer(parent);
+ viewer.setContentProvider(new CollectionContentProvider());
+ viewer.setInput(kinds);
+ viewer.setLabelProvider(new LabelProvider() {
+ @Override
+ public String getText(Object element) {
+ if (element instanceof String) {
+ String kind = (String) element;
+ IRepositoryManager manager = migrator.getRepositoryManager();
+ AbstractRepositoryConnector connector = manager.getRepositoryConnector(kind);
+ if (connector != null) {
+ return connector.getLabel()
+ + NLS.bind(" (used by {0} repositories)", manager.getRepositories(kind).size());
+ }
+ }
+ return super.getText(element);
+ }
+ });
+ return viewer;
+ }
+
+ private List<String> getRelevantConnectorKinds(Set<String> connectorKinds) {
+ IRepositoryManager manager = migrator.getRepositoryManager();
+ List<String> relevantConnectorKinds = new LinkedList<>();
+ for (String connectorKind : connectorKinds) {
+ if (!manager.getRepositories(connectorKind).isEmpty()) {
+ relevantConnectorKinds.add(connectorKind);
+ }
+ }
+ return relevantConnectorKinds;
+ }
+
+ @Override
+ public boolean performFinish() {
+ try {
+ getContainer().run(true, true, new IRunnableWithProgress() {
+
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
+ ImmutableList<String> connectors = FluentIterable.from(ImmutableList.copyOf(selectedConnectors))
+ .filter(String.class)
+ .toList();
+ try {
+ migrator.setConnectorsToMigrate(connectors);
+ migrator.migrateConnectors(monitor);
+ } catch (IOException e) {
+ throw new InvocationTargetException(e);
+ }
+ }
+ });
+ } catch (InvocationTargetException | InterruptedException e) {
+ StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, e.getMessage(), e));
+ IWizardPage page = getContainer().getCurrentPage();
+ if (page instanceof WizardPage && e.getCause() != null) {
+ ((WizardPage) page).setErrorMessage(e.getCause().getMessage());
+ }
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrator.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrator.java
new file mode 100644
index 000000000..a1d8b47a1
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/ConnectorMigrator.java
@@ -0,0 +1,426 @@
+/*******************************************************************************
+ * 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.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 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();
+ }
+ });
+
+ 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("Migrating repositories", oldRepositories.size() + 1);
+ getMigrationUi().backupTaskList(monitor);
+
+ for (TaskRepository repository : oldRepositories) {
+ if (monitor.isCanceled()) {
+ throw new OperationCanceledException();
+ }
+ monitor.subTask("Migrating " + 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));
+ }
+ disconnect(repository);
+ monitor.worked(1);
+ }
+
+ Set<TaskRepository> newRepositories = ImmutableSet.copyOf(repositories.values());
+ monitor.beginTask("Validating repository connections", newRepositories.size());
+ for (TaskRepository newRepository : newRepositories) {
+ if (monitor.isCanceled()) {
+ throw new OperationCanceledException();
+ }
+ monitor.subTask("Validating connection to " + 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 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(repository.getRepositoryLabel() + " (Unsupported, do not delete)");
+ 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("Migrating tasks for " + 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("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());
+ }
+
+ /**
+ * Connectors may override this method to support migrating tasks that are not contained in any migrated query.
+ */
+ protected TaskData getTaskData(String taskKey, AbstractRepositoryConnector newConnector,
+ TaskRepository newRepository, IProgressMonitor monitor) {
+ 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;
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/DefaultTasksState.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/DefaultTasksState.java
new file mode 100644
index 000000000..247d64be3
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/DefaultTasksState.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * 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 org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
+
+public class DefaultTasksState extends TasksState {
+
+ public DefaultTasksState() {
+ super(TasksUiPlugin.getTaskList(), TasksUiPlugin.getTaskDataManager(), TasksUiPlugin.getRepositoryManager(),
+ TasksUiPlugin.getRepositoryModel(), TasksUiPlugin.getContextStore(),
+ TasksUiPlugin.getTaskActivityManager(), TasksUiPlugin.getTaskJobFactory());
+ }
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/JobListener.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/JobListener.java
new file mode 100644
index 000000000..4beae16c5
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/JobListener.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * 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 java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+
+public class JobListener {
+
+ private final Set<Job> jobs = Collections.synchronizedSet(new HashSet<Job>());
+
+ private final Runnable allJobsDone;
+
+ private boolean started;
+
+ private boolean complete;
+
+ public JobListener(Runnable allJobsDone) {
+ this.allJobsDone = allJobsDone;
+ }
+
+ /**
+ * Must be called once when all jobs have been added.
+ */
+ public void start() {
+ synchronized (jobs) {
+ started = true;
+ if (jobs.isEmpty()) {
+ allJobsDone.run();
+ complete = true;
+ }
+ }
+ }
+
+ public boolean isComplete() {
+ return complete;
+ }
+
+ /**
+ * This method should only be called from a single thread.
+ */
+ public void add(final Job job, final Runnable jobDone) {
+ jobs.add(job);
+ job.addJobChangeListener(new JobChangeAdapter() {
+ @Override
+ public void done(IJobChangeEvent event) {
+ jobDone.run();
+ synchronized (jobs) {
+ jobs.remove(job);
+ if (jobs.isEmpty() && started) {
+ allJobsDone.run();
+ complete = true;
+ }
+ }
+ }
+ });
+ }
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/Messages.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/Messages.java
new file mode 100644
index 000000000..80a779d87
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/Messages.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * 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 org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "org.eclipse.mylyn.internal.tasks.ui.migrator.messages"; //$NON-NLS-1$
+
+ public static String CompleteConnectorMigrationWizard_Complete_Connector_Migration;
+
+ public static String CompleteConnectorMigrationWizard_Complete_Migration;
+
+ public static String CompleteConnectorMigrationWizard_first_page_message;
+
+ public static String CompleteConnectorMigrationWizard_Have_You_Recreated_Your_Queries;
+
+ public static String CompleteConnectorMigrationWizard_Migrate_Queries;
+
+ public static String CompleteConnectorMigrationWizard_Queries_Using_New_Connectors;
+
+ public static String CompleteConnectorMigrationWizard_Queries_Using_Old_Connectors;
+
+ public static String CompleteConnectorMigrationWizard_second_page_message;
+
+ public static String CompleteConnectorMigrationWizard_second_page_text;
+
+ public static String ConnectorMigrationWizard_Connector_Migration;
+
+ public static String ConnectorMigrationWizard_End_of_Connector_Support;
+
+ public static String ConnectorMigrationWizard_Message;
+
+ public static String ConnectorMigrationWizard_Body;
+
+ public static String ConnectorMigrationWizard_Select_Connectors;
+
+ public static String ConnectorMigrationWizard_Select_the_connectors_to_migrate;
+
+ public static String ConnectorMigrationWizard_validation_failed;
+
+ public static String ConnectorMigrator_complete_migration_prompt_message;
+
+ public static String ConnectorMigrator_complete_migration_prompt_title;
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TaskPredicates.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TaskPredicates.java
new file mode 100644
index 000000000..277c25d66
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TaskPredicates.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * 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 org.eclipse.mylyn.internal.tasks.core.AbstractTask;
+import org.eclipse.mylyn.internal.tasks.core.AbstractTaskCategory;
+import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
+import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
+import org.eclipse.mylyn.tasks.core.ITask;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+
+import com.google.common.base.Predicate;
+
+public class TaskPredicates {
+
+ private TaskPredicates() {
+ }
+
+ public static Predicate<AbstractTaskCategory> containsTask(final AbstractTask task) {
+ return new Predicate<AbstractTaskCategory>() {
+ @Override
+ public boolean apply(AbstractTaskCategory category) {
+ return category.contains(task.getHandleIdentifier());
+ }
+ };
+ }
+
+ public static Predicate<ITask> hasTaskKey(final String taskKey) {
+ return new Predicate<ITask>() {
+ @Override
+ public boolean apply(ITask task) {
+ return taskKey.equals(task.getTaskKey());
+ }
+ };
+ }
+
+ public static Predicate<ITask> isTaskForConnector(final String kind) {
+ return new Predicate<ITask>() {
+ @Override
+ public boolean apply(ITask task) {
+ return kind.equals(task.getConnectorKind());
+ }
+ };
+ }
+
+ public static Predicate<IRepositoryQuery> isQueryForRepository(final TaskRepository repository) {
+ return new Predicate<IRepositoryQuery>() {
+ @Override
+ public boolean apply(IRepositoryQuery query) {
+ return repository.getConnectorKind().equals(query.getConnectorKind())
+ && repository.getRepositoryUrl().equals(query.getRepositoryUrl());
+ }
+ };
+ }
+
+ public static Predicate<IRepositoryQuery> isQueryForConnector(final String kind) {
+ return new Predicate<IRepositoryQuery>() {
+ @Override
+ public boolean apply(IRepositoryQuery query) {
+ return kind.equals(query.getConnectorKind());
+ }
+ };
+ }
+
+ public static Predicate<RepositoryQuery> isSynchronizing() {
+ return new Predicate<RepositoryQuery>() {
+ @Override
+ public boolean apply(RepositoryQuery query) {
+ return query.isSynchronizing();
+ }
+ };
+ }
+
+ public static Predicate<AbstractTask> isTaskSynchronizing() {
+ return new Predicate<AbstractTask>() {
+ @Override
+ public boolean apply(AbstractTask task) {
+ return task.isSynchronizing();
+ }
+ };
+ }
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TasksState.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TasksState.java
new file mode 100644
index 000000000..8294f53df
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/TasksState.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * 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 org.eclipse.mylyn.internal.tasks.core.RepositoryModel;
+import org.eclipse.mylyn.internal.tasks.core.TaskActivityManager;
+import org.eclipse.mylyn.internal.tasks.core.TaskJobFactory;
+import org.eclipse.mylyn.internal.tasks.core.TaskList;
+import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryManager;
+import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
+import org.eclipse.mylyn.tasks.core.context.AbstractTaskContextStore;
+
+public class TasksState {
+
+ private final TaskActivityManager taskActivityManager;
+
+ private final TaskDataManager taskDataManager;
+
+ private final RepositoryModel repositoryModel;
+
+ private final TaskList taskList;
+
+ private final AbstractTaskContextStore contextStore;
+
+ private final TaskJobFactory taskJobFactory;
+
+ private final TaskRepositoryManager repositoryManager;
+
+ public TasksState(TaskList taskList, TaskDataManager taskDataManager, TaskRepositoryManager repositoryManager,
+ RepositoryModel repositoryModel, AbstractTaskContextStore contextStore,
+ TaskActivityManager taskActivityManager, TaskJobFactory taskJobFactory) {
+ this.taskList = taskList;
+ this.taskDataManager = taskDataManager;
+ this.repositoryManager = repositoryManager;
+ this.repositoryModel = repositoryModel;
+ this.contextStore = contextStore;
+ this.taskActivityManager = taskActivityManager;
+ this.taskJobFactory = taskJobFactory;
+ }
+
+ public TaskActivityManager getTaskActivityManager() {
+ return taskActivityManager;
+ }
+
+ public TaskDataManager getTaskDataManager() {
+ return taskDataManager;
+ }
+
+ public RepositoryModel getRepositoryModel() {
+ return repositoryModel;
+ }
+
+ public TaskList getTaskList() {
+ return taskList;
+ }
+
+ public AbstractTaskContextStore getContextStore() {
+ return contextStore;
+ }
+
+ public TaskJobFactory getTaskJobFactory() {
+ return taskJobFactory;
+ }
+
+ public TaskRepositoryManager getRepositoryManager() {
+ return repositoryManager;
+ }
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/messages.properties b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/messages.properties
new file mode 100644
index 000000000..543f4ad56
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/migrator/messages.properties
@@ -0,0 +1,46 @@
+CompleteConnectorMigrationWizard_Complete_Connector_Migration=Complete Connector Migration
+CompleteConnectorMigrationWizard_Complete_Migration=Complete Migration
+CompleteConnectorMigrationWizard_first_page_message=Migration will remove your old queries. Please ensure you have created the new \
+queries you want. Your old and new queries are shown below and you can edit them by double-clicking.
+CompleteConnectorMigrationWizard_Have_You_Recreated_Your_Queries=Have You Recreated Your Queries?
+CompleteConnectorMigrationWizard_Migrate_Queries=Migrate Queries
+CompleteConnectorMigrationWizard_Queries_Using_New_Connectors=Queries Using New Connectors
+CompleteConnectorMigrationWizard_Queries_Using_Old_Connectors=Queries Using Old Connectors
+CompleteConnectorMigrationWizard_second_page_message=Clicking finish will migrate your tasks and private data. This may take a while.
+CompleteConnectorMigrationWizard_second_page_text=When you click finish, your context, scheduled dates, private notes and other data \
+will be migrated to the new connectors. Any tasks \
+in your task list that are not included in the new queries you created will be downloaded using the new connectors. The old tasks, \
+queries, and repositories will be deleted.\n\
+\n\
+This may take a while. You should not use the task list or task editor while this is happening. You will be prompted when migration \
+is complete.\n\
+\n\
+You will be able to undo the migration by selecting "Restore Tasks from History" in the Task List view menu and choosing the \
+connector-migration-*.zip file stored in <workspace>/.metadata/.mylyn/backup. This will restore your task list and repositories to \
+the state they were in before the migration, but any data stored by 3rd party plugins for Mylyn may be lost.
+ConnectorMigrationWizard_Connector_Migration=Connector Migration
+ConnectorMigrationWizard_End_of_Connector_Support=End of Connector Support
+ConnectorMigrationWizard_Message=Support is ending for some connectors, but replacement connectors are installed. This wizard \
+will help you migrate your configuration and data to the new connectors.
+ConnectorMigrationWizard_Body=\{0}\n\
+\n\
+Your tasks, contexts, private notes, categories, scheduled dates, and private due dates \
+will be automatically migrated for all tasks. Incoming state will be migrated but all unsubmitted outgoing changes will be lost. \
+You may want to cancel this wizard and submit your outgoing changes before continuing with migration.\n\
+\n\
+This wizard will migrate your task repositories but you will need to manually migrate your queries before your tasks and private data \
+will be migrated. After finishing this wizard, please recreate your queries using the new connectors and then click the link in the \
+task list to complete the migration. The new connectors may not support the same queries as the old ones, \
+for example, you might need to create queries that use server-side searches or favorite filters that you create in the repository's \
+web UI.
+ConnectorMigrationWizard_Select_Connectors=Select Connectors
+ConnectorMigrationWizard_Select_the_connectors_to_migrate=Select the connectors to migrate. Your task list and repositories will be \
+backed up before migration; you can undo the migration \
+by selecting "Restore Tasks from History" in the Task List view menu and choosing the connector-migration-*.zip file stored \
+in <workspace>/.metadata/.mylyn/backup.
+ConnectorMigrationWizard_validation_failed=Could not validate the connection to the following repositories. Additional configuration may \
+be required. Please edit the repository settings via the Task Repositories view.\n\n{0}
+ConnectorMigrator_complete_migration_prompt_message=Support is ending for some of your connectors, but replacement connectors are \
+installed. <a href="{0}">Click here</a> for more information or to migrate to the new connectors.
+ConnectorMigrator_complete_migration_prompt_title=Connector migration is not finished. Manually recreate your queries using the new \
+connectors and then click <a href="{0}">complete migration</a> to migrate your contexts, scheduled dates, private notes and other data.

Back to the top