Bug 560062 - Always load both artifact and meta repos

Also try to clean up after yourself when adding repos temporarily
(But see caveats in RepositoryTracker:init())

Change-Id: Ib970be0b63ccc54f457ba95a8dea023eb5c003da
Signed-off-by: Carsten Reckord <reckord@yatta.de>
diff --git a/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/catalog/MarketplaceCatalog.java b/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/catalog/MarketplaceCatalog.java
index 1a3ab6c..39fcb99 100644
--- a/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/catalog/MarketplaceCatalog.java
+++ b/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/catalog/MarketplaceCatalog.java
@@ -33,6 +33,7 @@
 import org.eclipse.epp.internal.mpc.core.MarketplaceClientCore;
 import org.eclipse.epp.internal.mpc.ui.MarketplaceClientUi;
 import org.eclipse.epp.internal.mpc.ui.catalog.MarketplaceCategory.Contents;
+import org.eclipse.epp.internal.mpc.ui.operations.RepositoryTransactionHelper;
 import org.eclipse.epp.internal.mpc.ui.util.ConcurrentTaskManager;
 import org.eclipse.epp.mpc.core.model.ICategory;
 import org.eclipse.epp.mpc.core.model.IMarket;
@@ -53,6 +54,7 @@
 import org.eclipse.equinox.p2.query.IQuery;
 import org.eclipse.equinox.p2.query.IQueryResult;
 import org.eclipse.equinox.p2.query.QueryUtil;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
 import org.eclipse.equinox.p2.ui.ProvisioningUI;
@@ -298,7 +300,17 @@
 
 		ConcurrentTaskManager executor = new ConcurrentTaskManager(installedCatalogItemsByUpdateUri.size(),
 				Messages.MarketplaceCatalog_checkingForUpdates);
-		try {
+
+		ProvisioningSession session = ProvisioningUI.getDefaultUI().getSession();
+		IMetadataRepositoryManager metadataRepositoryManager = (IMetadataRepositoryManager) session
+				.getProvisioningAgent()
+				.getService(IMetadataRepositoryManager.SERVICE_NAME);
+		IArtifactRepositoryManager artifactRepositoryManager = (IArtifactRepositoryManager) session
+				.getProvisioningAgent()
+				.getService(IArtifactRepositoryManager.SERVICE_NAME);
+
+		try (RepositoryTransactionHelper repositories = new RepositoryTransactionHelper(metadataRepositoryManager,
+				artifactRepositoryManager)) {
 			final IProgressMonitor pm = new NullProgressMonitor() {
 				@Override
 				public boolean isCanceled() {
@@ -308,17 +320,30 @@
 			for (Map.Entry<URI, List<MarketplaceNodeCatalogItem>> entry : installedCatalogItemsByUpdateUri.entrySet()) {
 				final URI uri = entry.getKey();
 				final List<MarketplaceNodeCatalogItem> catalogItemsThisSite = entry.getValue();
+
+				//bug 560062 - add both artifact and metadata repo in case something breaks before we can clean up
+				repositories.addRepository(uri);
+
 				executor.submit(() -> {
-					ProvisioningSession session = ProvisioningUI.getDefaultUI().getSession();
-					IMetadataRepositoryManager manager = (IMetadataRepositoryManager) session.getProvisioningAgent()
-							.getService(IMetadataRepositoryManager.SERVICE_NAME);
 					try {
 						for (MarketplaceNodeCatalogItem item1 : catalogItemsThisSite) {
 							if (Boolean.TRUE.equals(item1.getAvailable())) {
 								item1.setAvailable(null);
 							}
 						}
-						IMetadataRepository repository = manager.loadRepository(uri, pm);
+						//bug 560062 - to be safe load the artifact repo as well, even though we don't need it here - and do it first because
+						//it's better to have artifact without meta than the other way around.
+						//see comments in RepositoryTransactionHelper.RepositoryTracker.init() for why we fully load artifact repos as well.
+						//TODO this needs a closer look - can we do without this without retriggering bug 560062? It's a serious performance killer...
+						artifactRepositoryManager.loadRepository(uri, pm);
+						if (pm.isCanceled()) {
+							return;
+						}
+						IMetadataRepository repository = metadataRepositoryManager.loadRepository(uri, pm);
+						if (pm.isCanceled()) {
+							return;
+						}
+
 						IQuery<IInstallableUnit> query = QueryUtil.createMatchQuery( //
 								"id ~= /*.feature.group/ && " + //$NON-NLS-1$
 								"properties['org.eclipse.equinox.p2.type.group'] == true ");//$NON-NLS-1$
diff --git a/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/operations/RepositoryTransactionHelper.java b/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/operations/RepositoryTransactionHelper.java
new file mode 100644
index 0000000..9e7eeb4
--- /dev/null
+++ b/org.eclipse.epp.mpc.ui/src/org/eclipse/epp/internal/mpc/ui/operations/RepositoryTransactionHelper.java
@@ -0,0 +1,198 @@
+/*******************************************************************************
+ * Copyright (c) 2018 The Eclipse Foundation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     The Eclipse Foundation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.epp.internal.mpc.ui.operations;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.equinox.p2.repository.IRepositoryManager;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
+import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
+
+public class RepositoryTransactionHelper implements AutoCloseable {
+
+	private final RepositoryTracker metadataRepositoryTracker;
+
+	private final RepositoryTracker artifactRepositoryTracker;
+
+	public RepositoryTransactionHelper(IMetadataRepositoryManager metadataRepositoryManager,
+			IArtifactRepositoryManager artifactRepositoryManager) {
+		metadataRepositoryTracker = metadataRepositoryManager == null ? null
+				: new RepositoryTracker(metadataRepositoryManager);
+		artifactRepositoryTracker = artifactRepositoryManager == null ? null
+				: new RepositoryTracker(artifactRepositoryManager);
+	}
+
+	public void init() {
+		if (metadataRepositoryTracker != null) {
+			metadataRepositoryTracker.init();
+		}
+		if (artifactRepositoryTracker != null) {
+			artifactRepositoryTracker.init();
+		}
+	}
+
+	public void addRepository(URI uri) {
+		//add artifact repo first, because if anything goes wrong it's better
+		//to have artifact without meta than the other way around
+		if (artifactRepositoryTracker != null) {
+			artifactRepositoryTracker.addRepository(uri);
+		}
+		if (metadataRepositoryTracker != null) {
+			metadataRepositoryTracker.addRepository(uri);
+		}
+	}
+
+	public void resetRepository(URI uri) {
+		if (metadataRepositoryTracker != null) {
+			metadataRepositoryTracker.resetRepository(uri);
+		}
+		if (artifactRepositoryTracker != null) {
+			artifactRepositoryTracker.resetRepository(uri);
+		}
+	}
+
+	public void resetAll() {
+		if (metadataRepositoryTracker != null) {
+			metadataRepositoryTracker.resetAll();
+		}
+		if (artifactRepositoryTracker != null) {
+			artifactRepositoryTracker.resetAll();
+		}
+	}
+
+	@Override
+	public void close() {
+		resetAll();
+	}
+
+	private enum CleanupAction {
+		REMOVE, DISABLE;
+	}
+
+	private static class RepositoryTracker implements AutoCloseable {
+		private static final int ENABLED_STATE_FLAGS = IRepositoryManager.REPOSITORIES_NON_LOCAL
+				| IRepositoryManager.REPOSITORIES_NON_SYSTEM;
+
+		private static final int DISABLED_STATE_FLAGS = IRepositoryManager.REPOSITORIES_DISABLED | ENABLED_STATE_FLAGS;
+
+		final IRepositoryManager<?> manager;
+
+		final Map<URI, CleanupAction> cleanupActions;
+
+		final Map<URI, Integer> initialState;
+
+		RepositoryTracker(IRepositoryManager<?> manager) {
+			this.manager = manager;
+			this.initialState = new HashMap<>();
+			this.cleanupActions = new LinkedHashMap<>();
+			init();
+		}
+
+		public void init() {
+			if (!cleanupActions.isEmpty()) {
+				throw new IllegalStateException();
+			}
+			initialState.clear();
+
+			/* TODO
+			 *
+			 * A lot of side effects are happening when repos are loaded - especially composites:
+			 * - child repos get added, sometimes as system=true/enabled=false, or sometimes as system=false/enabled=false
+			 * - repos might get enabled
+			 * - repos might get converted from system=true to system=false, also changing the type from
+			 *   LocalMetadataRepository/simpleRepository to something else
+			 *
+			 * All of this makes it rather hopeless to try and revert the repo state completely...
+			 * (For example, at the time of this writing, loading http://download.eclipse.org/mylyn/releases/latest
+			 * also creates http://download.eclipse.org/mylyn/releases/3.25 and leaves it behind as system=false/enabled=false,
+			 * adding it as a disabled repo to the visible repo list. It is hard to gauge if having those around in meta but not in
+			 * artifact would be safe - see bug 560062 - so it's probably best to always fully load both repos)
+			 *
+			 * That's why we only track some of the known repos and only revert what we explicitly did.
+			 *
+			 * We could possibly add a listener for RepositoryEvent.ADDED and relatives, but we might get false positives if
+			 * user does other repo things in parallel...
+			 */
+			URI[] enabledRemoteRepos = manager.getKnownRepositories(ENABLED_STATE_FLAGS);
+			URI[] disabledRemoteRepos = manager.getKnownRepositories(DISABLED_STATE_FLAGS);
+
+			for (URI uri : enabledRemoteRepos) {
+				initialState.put(uri, ENABLED_STATE_FLAGS);
+			}
+			for (URI uri : disabledRemoteRepos) {
+				initialState.put(uri, DISABLED_STATE_FLAGS);
+			}
+		}
+
+		public void addRepository(URI uri) {
+			manager.addRepository(uri);
+			mergeAction(uri, getCleanupAction(uri));
+		}
+
+		private void mergeAction(URI uri, CleanupAction cleanupAction) {
+			if (cleanupAction != null) {
+				cleanupActions.put(uri, cleanupAction);
+			}
+		}
+
+		private CleanupAction getCleanupAction(URI uri) {
+			Integer state = initialState.get(uri);
+			if (state == null) {
+				//did not exist initially
+				return CleanupAction.REMOVE;
+			}
+			if ((state & IRepositoryManager.REPOSITORIES_DISABLED) != 0) {
+				//initially disabled
+				return CleanupAction.DISABLE;
+			}
+			return null;
+		}
+
+		public void resetRepository(URI uri) {
+			CleanupAction action = cleanupActions.remove(uri);
+			doResetRepository(uri, action);
+		}
+
+		private void doResetRepository(URI uri, CleanupAction action) {
+			if (action == null) {
+				return;
+			}
+			switch (action) {
+			case REMOVE:
+				manager.removeRepository(uri);
+				break;
+			case DISABLE:
+				manager.setEnabled(uri, false);
+				break;
+			default:
+				throw new UnsupportedOperationException(action.name());
+			}
+		}
+
+		public void resetAll() {
+			for (Entry<URI, CleanupAction> entry : cleanupActions.entrySet()) {
+				doResetRepository(entry.getKey(), entry.getValue());
+			}
+			cleanupActions.clear();
+		}
+
+		@Override
+		public void close() {
+			resetAll();
+		}
+	}
+}