diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java index 35b25eee27d..f45cef246d9 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -39,6 +40,9 @@ import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.osgi.service.resolver.BundleDelta; import org.eclipse.osgi.service.resolver.BundleDescription; import org.eclipse.osgi.service.resolver.HostSpecification; @@ -129,6 +133,11 @@ public void removeModel(IPluginModelBase model) { private ArrayList fStateListeners; // a list of listeners interested in changes to the PDE/resolver State private boolean fCancelled = false; + /** + * only access synchronized with fEntriesSynchronizer + **/ + private Job fInitializationJob; + /** * Initialize the workspace and external (target) model manager * and add listeners to each one @@ -402,6 +411,50 @@ public boolean isInitialized() { } } + /** + * Initializes the master table in a background job unless it is initialized + * already, and runs the given callback as soon as the table is available. + *

+ * The calling thread is never used to resolve the target platform. The + * callback runs at most once, either directly on the calling thread when the + * table is already initialized, or on the job's thread. + *

+ */ + public void initializeInBackground(Runnable whenInitialized) { + Job job; + synchronized (fEntriesSynchronizer) { + if (fEntries != null) { + job = null; + } else { + if (fInitializationJob == null) { + fInitializationJob = Job.create(PDECoreMessages.PluginModelManager_InitializingPluginModels, + this::initialize); + fInitializationJob.setPriority(Job.LONG); + } + job = fInitializationJob; + } + } + if (job == null) { + whenInitialized.run(); + return; + } + AtomicBoolean notified = new AtomicBoolean(); + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + event.getJob().removeJobChangeListener(this); + if (notified.compareAndSet(false, true)) { + whenInitialized.run(); + } + } + }); + job.schedule(); + // the job may already have finished before the listener was attached + if (isInitialized() && notified.compareAndSet(false, true)) { + whenInitialized.run(); + } + } + /** * Returns whether the model initialization was cancelled by the user. * Other initializations, such as FeatureModelManager should use this diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java index 9624426f25a..7fc6d7e8e77 100644 --- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java @@ -38,6 +38,7 @@ import org.eclipse.pde.ui.tests.runtime.AllPDERuntimeTests; import org.eclipse.pde.ui.tests.search.dependencies.GatherUnusedDependenciesOperationTest; import org.eclipse.pde.ui.tests.target.AllTargetTests; +import org.eclipse.pde.ui.tests.util.PDELabelProviderTest; import org.eclipse.pde.ui.tests.views.log.AllLogViewTests; import org.eclipse.pde.ui.tests.wizards.AllNewProjectTests; import org.eclipse.ui.tests.smartimport.ProjectSmartImportTest; @@ -74,6 +75,7 @@ AllPDECoreTests.class, // ProjectSmartImportTest.class, // GatherUnusedDependenciesOperationTest.class, // + PDELabelProviderTest.class, // }) public class AllPDETests { diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java new file mode 100644 index 00000000000..673992850cc --- /dev/null +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.ui.tests.util; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.eclipse.pde.core.plugin.PluginRegistry; +import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.plugin.ImportObject; +import org.eclipse.pde.internal.core.plugin.PluginImport; +import org.eclipse.pde.internal.core.plugin.WorkspacePluginModel; +import org.eclipse.pde.internal.ui.PDELabelProvider; +import org.eclipse.pde.internal.ui.PDEPluginImages; +import org.eclipse.swt.graphics.Image; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies that {@link PDELabelProvider} does not resolve the target platform + * while decorating labels. + */ +public class PDELabelProviderTest { + + private static final String UNRESOLVED_ID = "org.eclipse.pde.tests.does.not.exist"; //$NON-NLS-1$ + + private ImportObject fUnresolvedImport; + + @Before + public void setUp() { + WorkspacePluginModel model = new WorkspacePluginModel(null, false); + fUnresolvedImport = new ImportObject(new PluginImport(model, UNRESOLVED_ID)); + } + + /** + * Once the models are available an unresolved import must still be decorated + * with the error overlay. + */ + @Test + public void testUnresolvedImportIsDecoratedWhenModelsAreAvailable() { + PluginRegistry.findEntry(UNRESOLVED_ID); + assertTrue("plug-in models should be initialized", //$NON-NLS-1$ + PDECore.getDefault().getModelManager().isInitialized()); + + PDELabelProvider labelProvider = new PDELabelProvider(); + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ); + assertNotSame("unresolved import should be decorated", plain, //$NON-NLS-1$ + labelProvider.getImage(fUnresolvedImport)); + } finally { + labelProvider.dispose(); + } + } + + /** + * As long as the models are not available the plain image must be returned + * instead of querying the plug-in registry, which would resolve the target + * platform on the calling thread. + */ + @Test + public void testImportIsUndecoratedWhileModelsAreUnavailable() { + PDELabelProvider labelProvider = new PDELabelProvider() { + @Override + protected boolean arePluginModelsAvailable() { + return false; + } + }; + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ); + assertSame("import must not be decorated before the models are known", plain, //$NON-NLS-1$ + labelProvider.getImage(fUnresolvedImport)); + } finally { + labelProvider.dispose(); + } + } +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java index a6150e5175e..e55bbddfe18 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java @@ -17,6 +17,7 @@ package org.eclipse.pde.internal.ui; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -25,6 +26,7 @@ import org.eclipse.jdt.ui.ISharedImages; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.LabelProviderChangedEvent; import org.eclipse.osgi.service.resolver.BundleDescription; import org.eclipse.osgi.service.resolver.ResolverError; import org.eclipse.osgi.util.NLS; @@ -46,6 +48,7 @@ import org.eclipse.pde.core.plugin.VersionMatchRule; import org.eclipse.pde.internal.core.ICoreConstants; import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.PluginModelManager; import org.eclipse.pde.internal.core.TargetPlatformHelper; import org.eclipse.pde.internal.core.WorkspaceModelManager; import org.eclipse.pde.internal.core.builders.CompilerFlags; @@ -90,6 +93,7 @@ import org.eclipse.pde.internal.ui.util.SWTUtil; import org.eclipse.pde.internal.ui.util.SharedLabelProvider; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import org.osgi.framework.Version; import org.osgi.resource.Resource; @@ -97,9 +101,43 @@ public class PDELabelProvider extends SharedLabelProvider { private static final String SYSTEM_BUNDLE = "system.bundle"; //$NON-NLS-1$ + private final AtomicBoolean fInitializationScheduled = new AtomicBoolean(); + public PDELabelProvider() { } + /** + * Returns whether the plug-in models can be queried without resolving the + * target platform, and schedules a background initialization if they cannot. + *

+ * Every lookup in the plug-in registry has to be guarded by this check: + * resolving the target platform can take minutes and label providers are + * called on the UI thread. Labels are refreshed once initialization has + * finished. + *

+ */ + protected boolean arePluginModelsAvailable() { + PluginModelManager manager = PDECore.getDefault().getModelManager(); + if (manager.isInitialized()) { + return true; + } + if (fInitializationScheduled.compareAndSet(false, true)) { + manager.initializeInBackground(this::refreshLabels); + } + return false; + } + + private void refreshLabels() { + fInitializationScheduled.set(false); + if (!PlatformUI.isWorkbenchRunning()) { + return; + } + Display display = PlatformUI.getWorkbench().getDisplay(); + if (!display.isDisposed()) { + display.asyncExec(() -> fireLabelProviderChanged(new LabelProviderChangedEvent(this))); + } + } + @Override public String getText(Object obj) { if (obj instanceof IPluginModelBase) { @@ -209,8 +247,14 @@ public String getObjectText(IPluginBase pluginBase) { } private String getSystemBundleInfo() { - IPluginBase systemBundle = PluginRegistry.findModel(SYSTEM_BUNDLE).getPluginBase(); - return NLS.bind(" [{0}]", systemBundle.getId()); //$NON-NLS-1$ + if (!arePluginModelsAvailable()) { + return ""; //$NON-NLS-1$ + } + IPluginModelBase model = PluginRegistry.findModel(SYSTEM_BUNDLE); + if (model == null) { + return ""; //$NON-NLS-1$ + } + return NLS.bind(" [{0}]", model.getPluginBase().getId()); //$NON-NLS-1$ } private String preventNull(String text) { @@ -253,7 +297,7 @@ public String getObjectText(IProductPlugin obj) { public String getObjectText(BundleDescription bundle) { String id = bundle.getSymbolicName(); - if (isFullNameModeEnabled()) { + if (isFullNameModeEnabled() && arePluginModelsAvailable()) { IPluginModelBase model = PluginRegistry.findModel((Resource) bundle); if (model != null) { return model.getPluginBase().getTranslatedName(); @@ -264,7 +308,7 @@ public String getObjectText(BundleDescription bundle) { } public String getObjectText(IPluginImport obj) { - if (isFullNameModeEnabled()) { + if (isFullNameModeEnabled() && arePluginModelsAvailable()) { String id = obj.getId(); IPluginModelBase model = PluginRegistry.findModel(obj.getId()); if (model != null) { @@ -314,7 +358,7 @@ private String getObjectText(Locale obj) { } public String getObjectText(FeaturePlugin obj) { - String name = isFullNameModeEnabled() ? obj.getLabel() : obj.getId(); + String name = isFullNameModeEnabled() && arePluginModelsAvailable() ? obj.getLabel() : obj.getId(); String version = obj.getVersion(); String text; @@ -330,7 +374,7 @@ public String getObjectText(FeaturePlugin obj) { public String getObjectText(FeatureImport obj) { int type = obj.getType(); if (type == IFeatureImport.PLUGIN) { - IPlugin plugin = obj.getPlugin(); + IPlugin plugin = arePluginModelsAvailable() ? obj.getPlugin() : null; if (plugin != null && isFullNameModeEnabled()) { return preventNull(plugin.getTranslatedName()); } @@ -397,10 +441,12 @@ public String getObjectText(ISiteFeature obj) { } public String getObjectText(ISiteBundle obj) { - IPluginModelBase modelBase = PluginRegistry.findModel(obj.getId(), obj.getVersion(), - VersionMatchRule.COMPATIBLE); - if (modelBase != null) { - return getObjectText(modelBase.getPluginBase()); + if (arePluginModelsAvailable()) { + IPluginModelBase modelBase = PluginRegistry.findModel(obj.getId(), obj.getVersion(), + VersionMatchRule.COMPATIBLE); + if (modelBase != null) { + return getObjectText(modelBase.getPluginBase()); + } } return preventNull(obj.getId()); } @@ -644,7 +690,9 @@ public Image getObjectImage(IFragment fragment, boolean checkEnabled, boolean ja private Image getObjectImage(ImportObject iobj) { int flags = 0; IPluginImport iimport = iobj.getImport(); - if (!iobj.isResolved()) { + // The resolved state is only known once the target platform is available + boolean modelsAvailable = arePluginModelsAvailable(); + if (modelsAvailable && !iobj.isResolved()) { flags = iimport.isOptional() ? F_WARNING : F_ERROR; } else if (iimport.isReexported()) { flags = F_EXPORT; @@ -652,7 +700,7 @@ private Image getObjectImage(ImportObject iobj) { if (iimport.isOptional()) { flags |= F_OPTIONAL; } - IPlugin plugin = iobj.getPlugin(); + IPlugin plugin = modelsAvailable ? iobj.getPlugin() : null; if (plugin != null) { IPluginModelBase model = plugin.getPluginModel(); flags |= getModelFlags(model); @@ -712,6 +760,9 @@ private Image getObjectImage(IPluginImport obj) { } private Image getObjectImage(IProductPlugin obj) { + if (!arePluginModelsAvailable()) { + return get(PDEPluginImages.DESC_PLUGIN_OBJ); + } Version version = (obj.getVersion() != null && obj.getVersion().length() > 0 && !obj.getVersion().equals(ICoreConstants.DEFAULT_VERSION)) ? Version.parseVersion(obj.getVersion()) : null; BundleDescription desc = TargetPlatformHelper.getState().getBundle(obj.getId(), version); if (desc != null) { @@ -781,7 +832,7 @@ private Image getObjectImage(IFeatureURLElement url) { private Image getObjectImage(IFeaturePlugin plugin) { int flags = 0; - if (((FeaturePlugin) plugin).getPluginBase() == null) { + if (arePluginModelsAvailable() && ((FeaturePlugin) plugin).getPluginBase() == null) { int cflag = CompilerFlags.getFlag(null, CompilerFlags.F_UNRESOLVED_PLUGINS); if (cflag == CompilerFlags.ERROR) { flags = F_ERROR; @@ -841,8 +892,7 @@ private Image getObjectImage(IFeatureImport obj) { } } else { base = PDEPluginImages.DESC_REQ_PLUGIN_OBJ; - IPlugin plugin = iimport.getPlugin(); - if (plugin == null) { + if (arePluginModelsAvailable() && iimport.getPlugin() == null) { flags = F_ERROR; } } @@ -907,7 +957,8 @@ public Image getObjectImage(PackageObject obj) { if (importPackageObject.isOptional()) { flags |= F_OPTIONAL; } - if (!importPackageObject.isResolved()) { + // The resolved state is only known once the target platform is available + if (arePluginModelsAvailable() && !importPackageObject.isResolved()) { flags |= importPackageObject.isOptional() ? F_WARNING : F_ERROR; } }