From 6d0064d00e2cb1e744941b8e060c0f5178b41711 Mon Sep 17 00:00:00 2001 From: Hannes Wellmann Date: Sat, 18 Nov 2023 21:16:49 +0100 Subject: [PATCH] [TP] Support no version and version ranges in target definitions Fixes https://github.com/eclipse-pde/eclipse.pde/issues/757 --- .../core/target/IUBundleContainer.java | 148 +++++++++++++----- .../core/target/IULocationFactory.java | 7 +- .../internal/core/target/P2TargetUtils.java | 80 +++++----- .../core/target/TargetPlatformService.java | 12 +- .../ui/shared/target/EditIUContainerPage.java | 111 ++++++++++++- .../internal/ui/shared/target/Messages.java | 2 + .../ui/shared/target/messages.properties | 2 + 7 files changed, 273 insertions(+), 89 deletions(-) diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IUBundleContainer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IUBundleContainer.java index 1ac6ab01cd..585b2204da 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IUBundleContainer.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IUBundleContainer.java @@ -15,6 +15,7 @@ * Manumitting Technologies Inc - bug 437726: wrong error messages opening target definition * Alexander Fedorov (ArSysOp) - Bug 542425, Bug 574629 * Christoph Läubrich - Bug 568865 - [target] add advanced editing capabilities for custom target platforms + * Hannes Wellmann - Support no version and version ranges in target definitions *******************************************************************************/ package org.eclipse.pde.internal.core.target; @@ -35,6 +36,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; @@ -54,9 +56,8 @@ import org.eclipse.equinox.p2.engine.IProfile; import org.eclipse.equinox.p2.metadata.IArtifactKey; import org.eclipse.equinox.p2.metadata.IInstallableUnit; -import org.eclipse.equinox.p2.metadata.IVersionedId; import org.eclipse.equinox.p2.metadata.Version; -import org.eclipse.equinox.p2.metadata.VersionedId; +import org.eclipse.equinox.p2.metadata.VersionRange; import org.eclipse.equinox.p2.query.IQuery; import org.eclipse.equinox.p2.query.IQueryResult; import org.eclipse.equinox.p2.query.IQueryable; @@ -79,6 +80,56 @@ */ public class IUBundleContainer extends AbstractBundleContainer { + record UnitDescription(String id, VersionRange version) { + private static final String EMPTY_VERSION = Version.emptyVersion.toString(); + + static UnitDescription parse(String id, String version) { + version = version.strip(); + if (version.isEmpty() || EMPTY_VERSION.equals(version)) { + return new UnitDescription(id, VersionRange.emptyRange); + } else if (version.contains(",")) { //$NON-NLS-1$ + return new UnitDescription(id, VersionRange.create(version)); + } else { + return create(id, Version.parseVersion(version)); + } + } + + static UnitDescription create(String id, Version version) { + VersionRange range = new VersionRange(version, true, version, true); + return new UnitDescription(id, range); + } + + UnitDescription { + Objects.requireNonNull(id); + Objects.requireNonNull(version); + } + + private boolean isSingleVersion() { + return version.equals(VersionRange.emptyRange) || // + (version.getMaximum().equals(version.getMinimum()) && version.getIncludeMinimum() + && version.getIncludeMaximum()); + } + + Optional getSingleVersion() { + return isSingleVersion() ? Optional.of(version.getMinimum()) : Optional.empty(); + } + + IQuery createIUQuery() { + return isSingleVersion() // + ? QueryUtil.createIUQuery(id, version.getMinimum()) + : QueryUtil.createIUQuery(id, version); + } + + String versionString() { + return getSingleVersion().map(Version::toString).orElseGet(version()::toString); + } + + @Override + public String toString() { + return VersionRange.emptyRange.equals(version) ? id : id + '/' + versionString(); + } + } + /** * Constant describing the type of bundle container */ @@ -120,13 +171,13 @@ public class IUBundleContainer extends AbstractBundleContainer { public static final int FOLLOW_REPOSITORY_REFERENCES = 1 << 4; /** The list of id+version of all root units declared in this container. */ - private final Set fIUs; + private final Set fIUs; /** * Cached IU's referenced by this bundle container, or null if not * resolved. */ - private List fUnits = List.of(); + private Map fUnits = Map.of(); /** * Repositories to consider, empty if default. @@ -166,7 +217,7 @@ public class IUBundleContainer extends AbstractBundleContainer { * {@link IUBundleContainer#INCLUDE_CONFIGURE_PHASE}, * {@link IUBundleContainer#FOLLOW_REPOSITORY_REFERENCES} */ - IUBundleContainer(List units, List repositories, int resolutionFlags) { + IUBundleContainer(List units, List repositories, int resolutionFlags) { fIUs = new LinkedHashSet<>(units); fFlags = resolutionFlags; fRepos = List.copyOf(repositories); @@ -264,17 +315,17 @@ protected TargetBundle[] resolveBundles(ITargetDefinition definition, IProgressM */ private void cacheIUs() throws CoreException { IProfile profile = fSynchronizer.getProfile(); - List result = new ArrayList<>(); + Map result = new LinkedHashMap<>(); MultiStatus status = new MultiStatus(PDECore.PLUGIN_ID, 0, Messages.IUBundleContainer_ProblemsLoadingRepositories); - for (IVersionedId unit : fIUs) { - IQuery query = QueryUtil.createIUQuery(unit); - addQueryResult(profile, query, unit, result, status); + for (UnitDescription unit : fIUs) { + IQuery query = unit.createIUQuery(); + getQueryResult(profile, query, unit, status).ifPresent(iu -> result.put(iu, unit)); } if (!status.isOK()) { fResolutionStatus = status; throw new CoreException(status); } - fUnits = List.copyOf(result); + fUnits = Collections.unmodifiableMap(result); } /** @@ -289,7 +340,7 @@ private void cacheBundles(ITargetDefinition target) throws CoreException { boolean onlyStrict = !fSynchronizer.getIncludeAllRequired(); IProfile metadata = fSynchronizer.getProfile(); PermissiveSlicer slicer = new PermissiveSlicer(metadata, new HashMap<>(), true, false, true, onlyStrict, false); - IQueryable slice = slicer.slice(fUnits, new NullProgressMonitor()); + IQueryable slice = slicer.slice(fUnits.keySet(), new NullProgressMonitor()); if (slicer.getStatus().getSeverity() == IStatus.ERROR) { // If the slicer has an error, report it instead of returning an empty set @@ -366,24 +417,29 @@ public synchronized IUBundleContainer update(Set toUpdate, IProgressMoni IQueryable source = P2TargetUtils.getQueryableMetadata(fRepos, isFollowRepositoryReferences(), progress.split(30)); boolean updated = false; - List updatedUnits = new ArrayList<>(fIUs); + List updatedUnits = new ArrayList<>(fIUs); SubMonitor loopProgress = progress.split(70).setWorkRemaining(updatedUnits.size()); for (int i = 0; i < updatedUnits.size(); i++) { - IVersionedId unit = updatedUnits.get(i); - if (!toUpdate.isEmpty() && !toUpdate.contains(unit.getId())) { + UnitDescription unit = updatedUnits.get(i); + if (!toUpdate.isEmpty() && !toUpdate.contains(unit.id())) { continue; + } else if (!unit.isSingleVersion()) { + // Don't update version ranges. They are usually intended to not + // use the latest version, so updating them should happen + // explicitly. } - IQuery query = QueryUtil.createLatestQuery(QueryUtil.createIUQuery(unit.getId())); + Version definedVersion = unit.getSingleVersion().get(); + IQuery query = QueryUtil.createLatestQuery(QueryUtil.createIUQuery(unit.id())); Optional queryResult = queryFirst(source, query, loopProgress.split(1)); Version updatedVersion = queryResult.map(IInstallableUnit::getVersion) // bail if the feature is no longer available. .orElseThrow(() -> new CoreException(Status.error(NLS.bind(Messages.IUBundleContainer_1, unit)))); // if the version is different from the spec (up or down), record the change. - if (!updatedVersion.equals(unit.getVersion())) { + if (!updatedVersion.equals(definedVersion)) { updated = true; // if the spec was not specific (e.g., 0.0.0) the target def itself has changed. - if (!unit.getVersion().equals(Version.emptyVersion)) { - updatedUnits.set(i, new VersionedId(unit.getId(), updatedVersion)); + if (!definedVersion.equals(Version.emptyVersion)) { + updatedUnits.set(i, UnitDescription.create(unit.id(), updatedVersion)); } } } @@ -487,7 +543,10 @@ public List getRepositories() { * @param unit unit to remove from the list of root IUs */ public synchronized void removeInstallableUnit(IInstallableUnit unit) { - fIUs.remove(new VersionedId(unit.getId(), unit.getVersion())); + UnitDescription description = fUnits.get(unit); + if (description != null) { + fIUs.remove(description); + } // Need to mark the container as unresolved clearResolutionStatus(); } @@ -572,12 +631,22 @@ public boolean isFollowRepositoryReferences() { * * @return the discovered IUs */ - public List getInstallableUnits() { - return fUnits; + public Collection getInstallableUnits() { + return fUnits.keySet(); + } + + public Map getInstallableUnitSpecifications() { + return fUnits.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> { + UnitDescription u = e.getValue(); + if (!u.isSingleVersion() || u.version().equals(VersionRange.emptyRange)) { + return u.version().toString(); + } + return ""; //$NON-NLS-1$ + })); } /** Returns the declared installable unit identifiers and versions. */ - Collection getDeclaredUnits() { + Collection getDeclaredUnits() { return Collections.unmodifiableSet(fIUs); } @@ -618,9 +687,12 @@ protected void associateWithTarget(ITargetDefinition target) { fSynchronizer.setFollowRepositoryReferences((fFlags & FOLLOW_REPOSITORY_REFERENCES) == FOLLOW_REPOSITORY_REFERENCES); } - private static final Comparator BY_ID_THEN_VERSION = Comparator // - .comparing(IVersionedId::getId) // - .thenComparing(IVersionedId::getVersion); + private static final Comparator BY_ID_THEN_VERSION = Comparator.comparing(UnitDescription::id) + // single versions first, then ranges + .thenComparing(u -> !u.isSingleVersion()) // false unit.version().getMinimum()) + // for single versions lower-bound == upper-bound + .thenComparing(unit -> unit.version().getMaximum()); @Override public String serialize() { @@ -663,8 +735,10 @@ public String serialize() { // Generate a predictable order of the elements fIUs.stream().sorted(BY_ID_THEN_VERSION).forEach(iu -> { Element unit = document.createElement(TargetDefinitionPersistenceHelper.INSTALLABLE_UNIT); - unit.setAttribute(TargetDefinitionPersistenceHelper.ATTR_ID, iu.getId()); - unit.setAttribute(TargetDefinitionPersistenceHelper.ATTR_VERSION, iu.getVersion().toString()); + unit.setAttribute(TargetDefinitionPersistenceHelper.ATTR_ID, iu.id()); + if (!iu.version().equals(VersionRange.emptyRange)) { + unit.setAttribute(TargetDefinitionPersistenceHelper.ATTR_VERSION, iu.versionString()); + } containerElement.appendChild(unit); }); try { @@ -681,15 +755,16 @@ public String serialize() { } } - Collection getRootIUs(IProgressMonitor monitor) throws CoreException { + Map> getRootIUs(IProgressMonitor monitor) throws CoreException { IQueryable repos = P2TargetUtils.getQueryableMetadata(getRepositories(), isFollowRepositoryReferences(), monitor); - MultiStatus status = new MultiStatus(PDECore.PLUGIN_ID, 0, Messages.IUBundleContainer_ProblemsLoadingRepositories); - List result = new ArrayList<>(); - for (IVersionedId iu : fIUs) { + MultiStatus status = new MultiStatus(PDECore.PLUGIN_ID, 0, Messages.IUBundleContainer_ProblemsLoadingRepositories, null); + Map> result = new HashMap<>(); + for (UnitDescription iu : fIUs) { // For versions such as 0.0.0, the IU query may return multiple IUs, so we check which is the latest version - IQuery query = QueryUtil.createLatestQuery(QueryUtil.createIUQuery(iu)); - addQueryResult(repos, query, iu, result, status); + IQuery query = QueryUtil.createLatestQuery(iu.createIUQuery()); + getQueryResult(repos, query, iu, status) + .ifPresent(unit -> result.computeIfAbsent(unit, u -> new HashSet<>(1)).add(iu.version())); } if (!status.isOK()) { fResolutionStatus = status; @@ -698,14 +773,13 @@ Collection getRootIUs(IProgressMonitor monitor) throws CoreExc return result; } - private void addQueryResult(IQueryable queryable, IQuery query, IVersionedId iu, - List result, MultiStatus status) { + private Optional getQueryResult(IQueryable queryable, + IQuery query, UnitDescription iu, MultiStatus status) { Optional queryResult = queryFirst(queryable, query, null); if (queryResult.isEmpty()) { status.add(Status.error(NLS.bind(Messages.IUBundleContainer_1, iu))); - } else { - result.add(queryResult.get()); } + return queryResult; } static Optional queryFirst(IQueryable queryable, IQuery query, IProgressMonitor monitor) { diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IULocationFactory.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IULocationFactory.java index e160f40c99..e0c09b7f47 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IULocationFactory.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/IULocationFactory.java @@ -72,11 +72,8 @@ public ITargetLocation getTargetLocation(String type, String serializedXML) thro if (element.getNodeName().equalsIgnoreCase(TargetDefinitionPersistenceHelper.INSTALLABLE_UNIT)) { String id = element.getAttribute(TargetDefinitionPersistenceHelper.ATTR_ID); if (id.length() > 0) { - String version = element.getAttribute(TargetDefinitionPersistenceHelper.ATTR_VERSION); - if (version.length() > 0) { - ids.add(id); - versions.add(version); - } + ids.add(id); + versions.add(element.getAttribute(TargetDefinitionPersistenceHelper.ATTR_VERSION)); } } else if (element.getNodeName().equalsIgnoreCase(TargetDefinitionPersistenceHelper.REPOSITORY)) { String loc = element.getAttribute(TargetDefinitionPersistenceHelper.LOCATION); diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java index 215a90e41c..4d60ede4ff 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/P2TargetUtils.java @@ -73,10 +73,10 @@ import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.metadata.IProvidedCapability; import org.eclipse.equinox.p2.metadata.IRequirement; -import org.eclipse.equinox.p2.metadata.IVersionedId; import org.eclipse.equinox.p2.metadata.MetadataFactory; import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitDescription; import org.eclipse.equinox.p2.metadata.Version; +import org.eclipse.equinox.p2.metadata.VersionRange; import org.eclipse.equinox.p2.planner.IPlanner; import org.eclipse.equinox.p2.planner.IProfileChangeRequest; import org.eclipse.equinox.p2.query.IQuery; @@ -96,9 +96,9 @@ import org.eclipse.pde.core.target.ITargetHandle; import org.eclipse.pde.core.target.ITargetLocation; import org.eclipse.pde.core.target.ITargetPlatformService; -import org.eclipse.pde.core.target.NameVersionDescriptor; import org.eclipse.pde.internal.core.ICoreConstants; import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.target.IUBundleContainer.UnitDescription; import org.eclipse.pde.internal.core.util.CoreUtility; import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; @@ -143,6 +143,13 @@ public class P2TargetUtils { */ static final String PROP_INSTALLED_IU = PDECore.PLUGIN_ID + ".installed_iu"; //$NON-NLS-1$ + /** + * Installable unit property to store the version-specifications of + * root/installed IU's that are declared in the target container as a + * semicolon separated list. + */ + static final String PROP_IU_VERSION_DECLARATION = PDECore.PLUGIN_ID + ".iu_version_declaration"; //$NON-NLS-1$ + /** * Profile property that keeps track of provisioning mode for the target * (slice versus plan). @@ -564,31 +571,24 @@ private boolean checkProfile(ITargetDefinition target, final IProfile profile) { // Check if each installed/root IU can be matched with exactly one // IU-declaration, if not the profile is not in sync anymore. - Set installedIUs = new HashSet<>(); + Map> installedIUs = new HashMap<>(); for (IInstallableUnit unit : queryResult) { - installedIUs.add(new NameVersionDescriptor(unit.getId(), unit.getVersion().toString())); - } - - Set emptyVersionIUs = new HashSet<>(); - for (IUBundleContainer iuContainer : iuContainers) { - for (IVersionedId iu : iuContainer.getDeclaredUnits()) { - // if there is something in a container but not in the profile, recreate - Version version = iu.getVersion(); - if (version.equals(Version.emptyVersion)) { - emptyVersionIUs.add(iu.getId()); - } else if (!installedIUs.remove(new NameVersionDescriptor(iu.getId(), version.toString()))) { - return false; + Set declarations = installedIUs.computeIfAbsent(unit.getId(), id -> new HashSet<>(1)); + String declaredVersions = profile.getInstallableUnitProperty(unit, PROP_IU_VERSION_DECLARATION); + if (declaredVersions == null) { + declarations.add(new VersionRange(unit.getVersion(), true, unit.getVersion(), true)); + } else { + for (String declaredVersion : declaredVersions.split(VERSION_DECLARATION_SEPARATOR)) { + declarations.add(VersionRange.create(declaredVersion)); } } } - if (!emptyVersionIUs.isEmpty()) { - // match remaining installed IUs with IUs declared with empty - // version. It's a full match if for each IU declared with empty - // version exactly one IU remains. - installedIUs.removeIf(iu -> emptyVersionIUs.remove(iu.getId())); - } - // If both are empty it's a full match and the profile checks out. - return emptyVersionIUs.isEmpty() && installedIUs.isEmpty(); + Map> declaredIUs = iuContainers.stream() // + .map(IUBundleContainer::getDeclaredUnits).flatMap(Collection::stream) // + .collect(Collectors.groupingBy(UnitDescription::id, + Collectors.mapping(UnitDescription::version, Collectors.toSet()))); + + return installedIUs.equals(declaredIUs); } private Stream iuBundleContainersOf(ITargetDefinition target) { @@ -1064,7 +1064,7 @@ private void resolveWithPlanner(ITargetDefinition target, IProfile profile, IPro SubMonitor subMonitor = SubMonitor.convert(monitor, Messages.IUBundleContainer_0, 220); // Get the root IUs for every relevant container in the target definition - Set units = getRootIUs(target, subMonitor.split(20)); + Map units = getRootIUs(target, subMonitor.split(20)); // create the provisioning plan IPlanner planner = getPlanner(); @@ -1072,10 +1072,11 @@ private void resolveWithPlanner(ITargetDefinition target, IProfile profile, IPro // first remove everything that was explicitly installed. Then add it back. This has the net effect of // removing everything that is no longer needed. computeRemovals(profile, request, getIncludeSource()); - request.addAll(units); - for (IInstallableUnit unit : units) { + request.addAll(units.keySet()); + units.forEach((unit, versionDeclarations) -> { request.setInstallableUnitProfileProperty(unit, PROP_INSTALLED_IU, Boolean.toString(true)); - } + request.setInstallableUnitProfileProperty(unit, PROP_IU_VERSION_DECLARATION, versionDeclarations); + }); List extraArtifactRepositories = new ArrayList<>(); List extraMetadataRepositories = new ArrayList<>(); @@ -1296,7 +1297,7 @@ private void resolveWithSlicer(ITargetDefinition target, IProfile profile, IProg SubMonitor subMonitor = SubMonitor.convert(monitor, Messages.IUBundleContainer_0, 110); // resolve IUs - Set units = getRootIUs(target, subMonitor.split(40)); + Map units = getRootIUs(target, subMonitor.split(40)); Collection repositories = getMetadataRepositories(target); if (repositories.isEmpty()) { @@ -1306,7 +1307,7 @@ private void resolveWithSlicer(ITargetDefinition target, IProfile profile, IProg subMonitor.split(5)); // do an initial slice to add everything the user requested - IQueryResult queryResult = slice(units, allMetadata, target, subMonitor.split(5)); + IQueryResult queryResult = slice(units.keySet(), allMetadata, target, subMonitor.split(5)); if (queryResult == null || queryResult.isEmpty()) { return; } @@ -1316,7 +1317,7 @@ private void resolveWithSlicer(ITargetDefinition target, IProfile profile, IProg if (getIncludeSource()) { // Build an IU that represents all the source bundles and slice again to add them in if available IInstallableUnit sourceIU = createSourceIU(queryResult, Version.createOSGi(1, 0, 0)); - List units2 = new ArrayList<>(units); + List units2 = new ArrayList<>(units.keySet()); units2.add(sourceIU); queryResult = slice(units2, allMetadata, target, subMonitor.split(5)); @@ -1338,9 +1339,10 @@ private void resolveWithSlicer(ITargetDefinition target, IProfile profile, IProg for (IInstallableUnit unit : newSet) { plan.addInstallableUnit(unit); } - for (IInstallableUnit unit : units) { + units.forEach((unit, versionDeclarations) -> { plan.setInstallableUnitProfileProperty(unit, PROP_INSTALLED_IU, Boolean.toString(true)); - } + plan.setInstallableUnitProfileProperty(unit, PROP_IU_VERSION_DECLARATION, versionDeclarations); + }); // remove all units that are in the current profile but not in the new slice Set toRemove = profile.query(QueryUtil.ALL_UNITS, null).toSet(); @@ -1521,6 +1523,8 @@ private void findProfileRepos(Set additionalRepos) { } } + private static final String VERSION_DECLARATION_SEPARATOR = ";"; //$NON-NLS-1$ + /** * Returns the IU's for the given target related to the given containers * @@ -1528,20 +1532,24 @@ private void findProfileRepos(Set additionalRepos) { * @return the discovered IUs * @exception CoreException if unable to retrieve IU's */ - private Set getRootIUs(ITargetDefinition definition, IProgressMonitor monitor) + private Map getRootIUs(ITargetDefinition definition, IProgressMonitor monitor) throws CoreException { ITargetLocation[] containers = definition.getTargetLocations(); if (containers == null) { - return Set.of(); + return Map.of(); } SubMonitor subMonitor = SubMonitor.convert(monitor, Messages.IUBundleContainer_0, containers.length); MultiStatus status = new MultiStatus(PDECore.PLUGIN_ID, 0, Messages.IUBundleContainer_ProblemsLoadingRepositories); - Set result = new HashSet<>(); + Map result = new HashMap<>(); for (ITargetLocation container : containers) { if (container instanceof IUBundleContainer iuContainer) { try { - result.addAll(iuContainer.getRootIUs(subMonitor.split(1))); + iuContainer.getRootIUs(subMonitor.split(1)).forEach((iu, versionDeclarations) -> { + String joindVersions = versionDeclarations.stream().map(VersionRange::toString) + .collect(Collectors.joining(VERSION_DECLARATION_SEPARATOR)); + result.merge(iu, joindVersions, (v1, v2) -> v1 + VERSION_DECLARATION_SEPARATOR + v2); + }); } catch (CoreException e) { status.add(e.getStatus()); } diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/TargetPlatformService.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/TargetPlatformService.java index d99ca4b00b..5e96746a92 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/TargetPlatformService.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/target/TargetPlatformService.java @@ -61,8 +61,6 @@ import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.equinox.frameworkadmin.BundleInfo; import org.eclipse.equinox.p2.metadata.IInstallableUnit; -import org.eclipse.equinox.p2.metadata.IVersionedId; -import org.eclipse.equinox.p2.metadata.VersionedId; import org.eclipse.osgi.service.datalocation.Location; import org.eclipse.osgi.util.NLS; import org.eclipse.pde.core.plugin.IPluginModelBase; @@ -78,6 +76,7 @@ import org.eclipse.pde.internal.core.PDEPreferencesManager; import org.eclipse.pde.internal.core.TargetDefinitionManager; import org.eclipse.pde.internal.core.TargetPlatformHelper; +import org.eclipse.pde.internal.core.target.IUBundleContainer.UnitDescription; import org.osgi.service.prefs.BackingStoreException; /** @@ -646,7 +645,8 @@ public IStatus compareWithTargetPlatform(ITargetDefinition target) throws CoreEx @Override public ITargetLocation newIULocation(IInstallableUnit[] units, URI[] repositories, int resolutionFlags) { - Stream ius = Arrays.stream(units).map(iu -> new VersionedId(iu.getId(), iu.getVersion())); + Stream ius = Arrays.stream(units) + .map(iu -> UnitDescription.create(iu.getId(), iu.getVersion())); return createIUBundleContainer(ius, repositories, resolutionFlags); } @@ -655,12 +655,12 @@ public ITargetLocation newIULocation(String[] unitIds, String[] versions, URI[] if (unitIds.length != versions.length) { throw new IllegalArgumentException("Units and versions must have the same length"); //$NON-NLS-1$ } - Stream ius = IntStream.range(0, unitIds.length) - .mapToObj(i -> new VersionedId(unitIds[i], versions[i])); + Stream ius = IntStream.range(0, unitIds.length) + .mapToObj(i -> UnitDescription.parse(unitIds[i], versions[i])); return createIUBundleContainer(ius, repositories, resolutionFlags); } - private ITargetLocation createIUBundleContainer(Stream ius, URI[] repos, int resolutionFlags) { + private ITargetLocation createIUBundleContainer(Stream ius, URI[] repos, int resolutionFlags) { return new IUBundleContainer(ius.toList(), repos == null ? List.of() : List.of(repos), resolutionFlags); } diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditIUContainerPage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditIUContainerPage.java index 3dc0f6f519..85d20899f5 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditIUContainerPage.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/EditIUContainerPage.java @@ -19,7 +19,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.runtime.CoreException; @@ -28,14 +30,22 @@ import org.eclipse.equinox.internal.p2.ui.ProvUI; import org.eclipse.equinox.internal.p2.ui.ProvUIMessages; import org.eclipse.equinox.internal.p2.ui.dialogs.AvailableIUGroup; +import org.eclipse.equinox.internal.p2.ui.dialogs.ContainerCheckedTreeViewer; import org.eclipse.equinox.internal.p2.ui.dialogs.RepositorySelectionGroup; +import org.eclipse.equinox.internal.p2.ui.model.AvailableIUElement; import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.operations.ProvisioningSession; import org.eclipse.equinox.p2.ui.Policy; import org.eclipse.equinox.p2.ui.ProvisioningUI; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.StructuredViewer; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.window.SameShellProvider; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.osgi.util.NLS; @@ -45,6 +55,7 @@ import org.eclipse.pde.internal.core.PDECore; import org.eclipse.pde.internal.core.target.IUBundleContainer; import org.eclipse.pde.internal.core.target.P2TargetUtils; +import org.eclipse.pde.internal.core.util.VersionUtil; import org.eclipse.pde.internal.ui.IHelpContextIds; import org.eclipse.pde.internal.ui.PDEPlugin; import org.eclipse.pde.internal.ui.SWTFactory; @@ -58,6 +69,7 @@ import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.PlatformUI; +import org.osgi.framework.Version; /** * Wizard page allowing users to select which IUs they would like to download @@ -103,6 +115,7 @@ public class EditIUContainerPage extends WizardPage implements IEditBundleContai private RepositorySelectionGroup fRepoSelector; private AvailableIUGroup fAvailableIUGroup; + private Map versionSpecifications = new HashMap<>(); private Label fSelectionCount; private Button fPropertiesButton; private IAction fPropertyAction; @@ -158,8 +171,24 @@ public ITargetLocation getBundleContainer() { flags |= fIncludeSourceButton.getSelection() ? IUBundleContainer.INCLUDE_SOURCE : 0; flags |= fConfigurePhaseButton.getSelection() ? IUBundleContainer.INCLUDE_CONFIGURE_PHASE : 0; flags |= fFollowRepositoryReferencesButton.getSelection() ? IUBundleContainer.FOLLOW_REPOSITORY_REFERENCES : 0; - return service.newIULocation(fAvailableIUGroup.getCheckedLeafIUs(), - fRepoLocation != null ? new URI[] { fRepoLocation } : null, flags); + + IInstallableUnit[] selectedIUs = fAvailableIUGroup.getCheckedLeafIUs(); + URI[] repositories = fRepoLocation != null ? new URI[] { fRepoLocation } : null; + if (versionSpecifications != null && !versionSpecifications.isEmpty()) { + String[] ids = new String[selectedIUs.length]; + String[] versions = new String[selectedIUs.length]; + for (int i = 0; i < selectedIUs.length; i++) { + IInstallableUnit iu = selectedIUs[i]; + ids[i] = iu.getId(); + String version = versionSpecifications.get(iu); + if (version == null || version.isBlank()) { + version = iu.getVersion().toString(); + } + versions[i] = version; + } + return service.newIULocation(ids, versions, repositories, flags); + } + return service.newIULocation(selectedIUs, repositories, flags); } @Override @@ -237,7 +266,7 @@ private void refreshAvailableIUArea(final Composite parent) { final String pendingLabel = org.eclipse.ui.internal.progress.ProgressMessages.PendingUpdateAdapter_PendingLabel; if (children.length > 0 && !children[0].getText().equals(pendingLabel)) { fAvailableIUGroup.getCheckboxTreeViewer().expandAll(); - fAvailableIUGroup.setChecked(fEditContainer.getInstallableUnits().toArray()); + setInstallableUnits(fEditContainer); fAvailableIUGroup.getCheckboxTreeViewer().collapseAll(); loaded.set(true); } @@ -262,7 +291,74 @@ private void createAvailableIUArea(Composite parent) { if (!profileUI.getPolicy().getRepositoriesVisible()) { filterConstant = AvailableIUGroup.AVAILABLE_ALL; } - fAvailableIUGroup = new AvailableIUGroup(profileUI, parent, parent.getFont(), fQueryContext, null, filterConstant); + fAvailableIUGroup = new AvailableIUGroup(profileUI, parent, parent.getFont(), fQueryContext, null, + filterConstant) { + + @Override + protected StructuredViewer createViewer(Composite parent) { + ContainerCheckedTreeViewer treeViewer = (ContainerCheckedTreeViewer) super.createViewer(parent); + if (fEditContainer != null) { + TreeViewerColumn column = new TreeViewerColumn(treeViewer, SWT.NONE, getColumnConfig().length); + column.getColumn().setText(Messages.EditIUContainerPage_VersionSpecification_Label); + column.getColumn().setWidth(150); // TODO: fit to size?! + column.getColumn().setResizable(true); + CellEditor versionSpecEditor = new TextCellEditor(treeViewer.getTree()); + versionSpecEditor.setValidator(this::validateVersionSpecification); + column.setEditingSupport(new EditingSupport(treeViewer) { + @Override + @SuppressWarnings("restriction") + protected void setValue(Object element, Object value) { + if (element instanceof AvailableIUElement iuElement && value instanceof String spec) { + spec = sanitizeVersionSpecification(spec); + versionSpecifications.put(iuElement.getIU(), spec); + treeViewer.update(iuElement, null); + } + } + + @Override + protected Object getValue(Object element) { + return getVersionSpecification(element); + } + + @Override + protected boolean canEdit(Object element) { + return element instanceof @SuppressWarnings("restriction") AvailableIUElement iuElement + && treeViewer.getChecked(iuElement); + } + + @Override + protected CellEditor getCellEditor(Object element) { + return versionSpecEditor; + } + }); + column.setLabelProvider(ColumnLabelProvider.createTextProvider(this::getVersionSpecification)); + } + return treeViewer; + } + + private static final String EMPTY_VERSION = Version.emptyVersion.toString(); + private static final String LATEST_LABEL = Messages.EditIUContainerPage_Latest_Label; + + private String sanitizeVersionSpecification(String spec) { + spec = spec.strip(); + return LATEST_LABEL.equals(spec) ? EMPTY_VERSION : spec; + } + + @SuppressWarnings("restriction") + private String getVersionSpecification(Object e) { + String spec = e instanceof AvailableIUElement iu ? versionSpecifications.getOrDefault(iu.getIU(), "") //$NON-NLS-1$ + : ""; //$NON-NLS-1$ + return EMPTY_VERSION.equals(spec) ? LATEST_LABEL : spec; + } + + private String validateVersionSpecification(Object value) { + if (LATEST_LABEL.equals(value)) { + return null; + } + IStatus result = VersionUtil.validateVersionRange((String) value); + return result.isOK() ? null : result.getMessage(); + } + }; fAvailableIUGroup.getCheckboxTreeViewer().addCheckStateListener(event -> { IInstallableUnit[] units = fAvailableIUGroup.getCheckedLeafIUs(); if (units.length > 0) { @@ -612,7 +708,7 @@ private void restoreWidgetState() { // Only able to check items if we don't have categories fQueryContext.setViewType(org.eclipse.equinox.internal.p2.ui.query.IUViewQueryContext.AVAILABLE_VIEW_FLAT); fAvailableIUGroup.updateAvailableViewState(); - fAvailableIUGroup.setChecked(fEditContainer.getInstallableUnits().toArray()); + setInstallableUnits(fEditContainer); // Make sure view is back in proper state updateViewContext(); IInstallableUnit[] units = fAvailableIUGroup.getCheckedLeafIUs(); @@ -625,4 +721,9 @@ private void restoreWidgetState() { fAvailableIUGroup.getCheckboxTreeViewer().collapseAll(); } } + + private void setInstallableUnits(IUBundleContainer iuContainer) { + versionSpecifications = new HashMap<>(iuContainer.getInstallableUnitSpecifications()); + fAvailableIUGroup.setChecked(versionSpecifications.keySet().toArray()); + } } \ No newline at end of file diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java index c5e186604a..a245d5dbb5 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/Messages.java @@ -104,6 +104,8 @@ public class Messages extends NLS { public static String EditIUContainerPage_IncludeConfigurePhase; public static String EditIUContainerPage_itemSelected; public static String EditIUContainerPage_itemsSelected; + public static String EditIUContainerPage_VersionSpecification_Label; + public static String EditIUContainerPage_Latest_Label; public static String EditProfileContainerPage_1; public static String EditProfileContainerPage_2; public static String EditProfileContainerPage_3; diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties index 85cd2ce029..671541550e 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/shared/target/messages.properties @@ -99,6 +99,8 @@ EditIUContainerPage_9=The target platform service could not be acquired. EditIUContainerPage_IncludeConfigurePhase=Include configure phase EditIUContainerPage_itemSelected={0} item selected EditIUContainerPage_itemsSelected={0} items selected +EditIUContainerPage_VersionSpecification_Label=Version specification +EditIUContainerPage_Latest_Label=latest EditProfileContainerPage_1=Var&iables... EditProfileContainerPage_2=Select a configuration directory EditProfileContainerPage_3=Edit Installation