From 3207819ccbda7d5fa83fac5937e4c390b4f8e604 Mon Sep 17 00:00:00 2001
From: Hannes Wellmann <wellmann.hannes1@gmx.net>
Date: Sat, 19 Oct 2024 13:26:20 +0200
Subject: [PATCH] [TP-Editor] Propose children of composite repositories

---
 .../AttributeValueCompletionProcessor.java    |  6 ++++
 .../extension/model/RepositoryCache.java      | 34 +++++++++++++------
 .../target/extension/p2/P2Fetcher.java        | 26 +++++++++++---
 3 files changed, 52 insertions(+), 14 deletions(-)

diff --git a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/autocomplete/processors/AttributeValueCompletionProcessor.java b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/autocomplete/processors/AttributeValueCompletionProcessor.java
index 24224154d3b..525623cd1d8 100644
--- a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/autocomplete/processors/AttributeValueCompletionProcessor.java
+++ b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/autocomplete/processors/AttributeValueCompletionProcessor.java
@@ -15,6 +15,7 @@
  *******************************************************************************/
 package org.eclipse.pde.internal.genericeditor.target.extension.autocomplete.processors;
 
+import java.net.URI;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
@@ -111,6 +112,11 @@ public ICompletionProposal[] getCompletionProposals() {
 			}
 		}
 
+		if (ITargetConstants.REPOSITORY_LOCATION_ATTR.equalsIgnoreCase(acKey)) {
+			List<URI> children = RepositoryCache.fetchChildrenOfRepo(searchTerm);
+			return toProposals(children.stream().map(URI::toString));
+		}
+
 		return new ICompletionProposal[] {};
 	}
 
diff --git a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/model/RepositoryCache.java b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/model/RepositoryCache.java
index 9cde42368ea..c11caa1211c 100644
--- a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/model/RepositoryCache.java
+++ b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/model/RepositoryCache.java
@@ -24,15 +24,18 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.eclipse.core.runtime.ILog;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.equinox.p2.metadata.IVersionedId;
+import org.eclipse.equinox.p2.metadata.VersionedId;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.pde.internal.genericeditor.target.extension.p2.Messages;
 import org.eclipse.pde.internal.genericeditor.target.extension.p2.P2Fetcher;
+import org.eclipse.pde.internal.genericeditor.target.extension.p2.P2Fetcher.RepositoryContent;
 
 /**
  * This class is used to cache the p2 repositories completion information order
@@ -48,7 +51,10 @@ private RepositoryCache() {
 		// avoid instantiation
 	}
 
-	private static final Map<URI, CompletableFuture<Map<String, List<IVersionedId>>>> CACHE = new ConcurrentHashMap<>();
+	private static record RepositoryMetadata(Map<String, List<IVersionedId>> units, List<URI> children) {
+	}
+
+	private static final Map<URI, CompletableFuture<RepositoryMetadata>> CACHE = new ConcurrentHashMap<>();
 
 	/**
 	 * Fetches information and caches it.
@@ -64,19 +70,24 @@ private RepositoryCache() {
 	 */
 	public static Map<String, List<IVersionedId>> fetchP2UnitsFromRepos(List<String> repositories) {
 		if (repositories.size() == 1) {
-			return getFutureValue(fetchP2DataOfRepo(repositories.get(0)));
+			return getFutureValue(fetchP2DataOfRepo(repositories.get(0)), RepositoryMetadata::units, Map.of());
 		}
 		var repos = repositories.stream().map(RepositoryCache::fetchP2DataOfRepo).toList();
 		// Fetch all repos at once to await pending metadata in parallel
-		return toSortedMap(repos.stream().map(RepositoryCache::getFutureValue) //
+		return toSortedMap(repos.stream()
+				.map(r -> getFutureValue(r, RepositoryMetadata::units, Map.<String, List<IVersionedId>>of()))
 				.map(Map::values).flatMap(Collection::stream).flatMap(List::stream));
 	}
 
+	public static List<URI> fetchChildrenOfRepo(String repository) {
+		return getFutureValue(fetchP2DataOfRepo(repository), RepositoryMetadata::children, List.of());
+	}
+
 	public static void prefetchP2MetadataOfRepository(String repository) {
 		fetchP2DataOfRepo(repository);
 	}
 
-	private static Future<Map<String, List<IVersionedId>>> fetchP2DataOfRepo(String repository) {
+	private static Future<RepositoryMetadata> fetchP2DataOfRepo(String repository) {
 		URI location;
 		try {
 			location = new URI(repository);
@@ -87,13 +98,15 @@ private static Future<Map<String, List<IVersionedId>>> fetchP2DataOfRepo(String
 			if (f != null && (!f.isDone() || !f.isCompletedExceptionally() && !f.isCancelled())) {
 				return f; // computation is running or has succeeded
 			}
-			CompletableFuture<Map<String, List<IVersionedId>>> future = new CompletableFuture<>();
+			CompletableFuture<RepositoryMetadata> future = new CompletableFuture<>();
 			// Fetching P2 repository information is a costly operation
 			// time-wise. Thus it is done in a job.
 			Job job = Job.create(NLS.bind(Messages.UpdateJob_P2DataFetch, repo), m -> {
 				try {
-					Map<String, List<IVersionedId>> units = toSortedMap(P2Fetcher.fetchAvailableUnits(repo, m));
-					future.complete(units);
+					RepositoryContent content = P2Fetcher.fetchAvailableUnits(repo, m);
+					Map<String, List<IVersionedId>> units = toSortedMap(
+							content.units().stream().map(iu -> new VersionedId(iu.getId(), iu.getVersion())));
+					future.complete(new RepositoryMetadata(units, content.children()));
 				} catch (Throwable e) {
 					future.completeExceptionally(e);
 					// Only log the failure, don't open an error-dialog.
@@ -115,11 +128,12 @@ private static Map<String, List<IVersionedId>> toSortedMap(Stream<IVersionedId>
 				Collectors.groupingBy(IVersionedId::getId, LinkedHashMap::new, Collectors.toUnmodifiableList()));
 	}
 
-	private static Map<String, List<IVersionedId>> getFutureValue(Future<Map<String, List<IVersionedId>>> future) {
+	private static <T> T getFutureValue(Future<RepositoryMetadata> future, Function<RepositoryMetadata, T> getter,
+			T defaultValue) {
 		try {
-			return future.get();
+			return getter.apply(future.get());
 		} catch (Exception e) { // interrupted, canceled or execution failure
-			return Map.of();
+			return defaultValue;
 		}
 	}
 
diff --git a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/p2/P2Fetcher.java b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/p2/P2Fetcher.java
index cf09b8cf4c5..c08e5799544 100644
--- a/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/p2/P2Fetcher.java
+++ b/ui/org.eclipse.pde.genericeditor.extension/src/org/eclipse/pde/internal/genericeditor/target/extension/p2/P2Fetcher.java
@@ -14,6 +14,7 @@
 package org.eclipse.pde.internal.genericeditor.target.extension.p2;
 
 import java.net.URI;
+import java.util.List;
 import java.util.stream.Stream;
 
 import org.eclipse.core.runtime.CoreException;
@@ -21,11 +22,12 @@
 import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.equinox.p2.core.IProvisioningAgent;
 import org.eclipse.equinox.p2.core.IProvisioningAgentProvider;
+import org.eclipse.equinox.p2.core.ProvisionException;
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
-import org.eclipse.equinox.p2.metadata.IVersionedId;
-import org.eclipse.equinox.p2.metadata.VersionedId;
 import org.eclipse.equinox.p2.query.IQueryResult;
 import org.eclipse.equinox.p2.query.QueryUtil;
+import org.eclipse.equinox.p2.repository.ICompositeRepository;
+import org.eclipse.equinox.p2.repository.IRepository;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
 import org.eclipse.pde.internal.genericeditor.target.extension.model.UnitNode;
@@ -39,6 +41,9 @@
  */
 public class P2Fetcher {
 
+	public static record RepositoryContent(IQueryResult<IInstallableUnit> units, List<URI> children) {
+	}
+
 	/**
 	 * This methods goes 'online' to make contact with a p2 repo and query it.
 	 *
@@ -46,7 +51,7 @@ public class P2Fetcher {
 	 *            URL string of a p2 repository
 	 * @return List of available installable unit models. See {@link UnitNode}
 	 */
-	public static Stream<IVersionedId> fetchAvailableUnits(URI repositoryLocation, IProgressMonitor monitor)
+	public static RepositoryContent fetchAvailableUnits(URI repositoryLocation, IProgressMonitor monitor)
 			throws CoreException {
 		SubMonitor subMonitor = SubMonitor.convert(monitor, 31);
 		BundleContext context = FrameworkUtil.getBundle(P2Fetcher.class).getBundleContext();
@@ -57,10 +62,23 @@ public static Stream<IVersionedId> fetchAvailableUnits(URI repositoryLocation, I
 			IMetadataRepositoryManager manager = agent.getService(IMetadataRepositoryManager.class);
 			IMetadataRepository repository = manager.loadRepository(repositoryLocation, subMonitor.split(30));
 			IQueryResult<IInstallableUnit> allUnits = repository.query(QueryUtil.ALL_UNITS, subMonitor.split(1));
-			return allUnits.stream().map(iu -> new VersionedId(iu.getId(), iu.getVersion()));
+			List<URI> children = allChildren(repository, manager).toList();
+			return new RepositoryContent(allUnits, children);
 		} finally {
 			context.ungetService(sr);
 		}
 	}
 
+	private static Stream<URI> allChildren(IRepository<?> repository, IMetadataRepositoryManager manager) {
+		if (repository instanceof ICompositeRepository<?> composite) {
+			return composite.getChildren().stream().flatMap(uri -> {
+				try { // repository should already been cached
+					return Stream.concat(Stream.of(uri), allChildren(manager.loadRepository(uri, null), manager));
+				} catch (ProvisionException e) {
+					return Stream.of(uri);
+				}
+			});
+		}
+		return Stream.empty();
+	}
 }