| /******************************************************************************* |
| * Copyright (c) 2000, 2022 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| * Les Jones <lesojones@gmail.com> - bug 191365 |
| *******************************************************************************/ |
| package org.eclipse.pde.internal.core; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.MultiStatus; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jdt.core.IClasspathContainer; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.osgi.service.resolver.BundleDelta; |
| import org.eclipse.osgi.service.resolver.BundleDescription; |
| import org.eclipse.osgi.service.resolver.HostSpecification; |
| import org.eclipse.osgi.service.resolver.PlatformAdmin; |
| import org.eclipse.osgi.service.resolver.StateDelta; |
| import org.eclipse.pde.core.IModel; |
| import org.eclipse.pde.core.IModelProviderEvent; |
| import org.eclipse.pde.core.IModelProviderListener; |
| import org.eclipse.pde.core.build.IBuild; |
| import org.eclipse.pde.core.build.IBuildEntry; |
| import org.eclipse.pde.core.plugin.IPluginModel; |
| import org.eclipse.pde.core.plugin.IPluginModelBase; |
| import org.eclipse.pde.core.plugin.ModelEntry; |
| import org.eclipse.pde.core.target.ITargetDefinition; |
| import org.eclipse.pde.core.target.LoadTargetDefinitionJob; |
| import org.eclipse.pde.core.target.TargetBundle; |
| import org.eclipse.pde.internal.core.target.P2TargetUtils; |
| |
| public class PluginModelManager implements IModelProviderListener { |
| private static final String fExternalPluginListFile = "SavedExternalPluginList.txt"; //$NON-NLS-1$ |
| private static PluginModelManager fModelManager; |
| |
| /** |
| * Job to update class path containers asynchronously. Avoids blocking the UI thread |
| * while saving the manifest editor. The job is given a workspace lock so other jobs can't |
| * run on a stale classpath. |
| */ |
| class UpdateClasspathsJob extends Job { |
| |
| private final List<IJavaProject> fProjects = new ArrayList<>(); |
| private final List<IClasspathContainer> fContainers = new ArrayList<>(); |
| |
| /** |
| * Constructs a new job. |
| */ |
| public UpdateClasspathsJob() { |
| super(PDECoreMessages.PluginModelManager_1); |
| // The job is given a workspace lock so other jobs can't run on a stale classpath (bug 354993) |
| setRule(ResourcesPlugin.getWorkspace().getRoot()); |
| } |
| |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| try { |
| boolean more = false; |
| do { |
| IJavaProject[] projects = null; |
| IClasspathContainer[] containers = null; |
| synchronized (fProjects) { |
| projects = fProjects.toArray(new IJavaProject[fProjects.size()]); |
| containers = fContainers.toArray(new IClasspathContainer[fContainers.size()]); |
| fProjects.clear(); |
| fContainers.clear(); |
| } |
| JavaCore.setClasspathContainer(PDECore.REQUIRED_PLUGINS_CONTAINER_PATH, projects, containers, monitor); |
| synchronized (fProjects) { |
| more = !fProjects.isEmpty(); |
| } |
| } while (more); |
| |
| } catch (JavaModelException e) { |
| return e.getStatus(); |
| } |
| return Status.OK_STATUS; |
| } |
| |
| /** |
| * Queues more projects/containers. |
| * |
| * @param projects |
| * @param containers |
| */ |
| void add(IJavaProject[] projects, IClasspathContainer[] containers) { |
| synchronized (fProjects) { |
| for (int i = 0; i < containers.length; i++) { |
| fProjects.add(projects[i]); |
| fContainers.add(containers[i]); |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * Job used to update class path containers. |
| */ |
| private final UpdateClasspathsJob fUpdateJob = new UpdateClasspathsJob(); |
| |
| /** |
| * Subclass of ModelEntry |
| * It adds methods that add/remove model from the entry. |
| * These methods must not be on ModelEntry itself because |
| * ModelEntry is an API class and we do not want clients to manipulate |
| * the ModelEntry |
| * |
| */ |
| private class LocalModelEntry extends ModelEntry { |
| |
| /** |
| * Constructs a model entry that will keep track |
| * of all bundles in the workspace and target that share the same ID. |
| * |
| * @param id the bundle ID |
| */ |
| public LocalModelEntry(String id) { |
| super(id); |
| } |
| |
| /** |
| * Adds a model to the entry. |
| * An entry keeps two lists: one for workspace models |
| * and one for target (external) models. |
| * If the model being added is associated with a workspace resource, |
| * it is added to the workspace list; otherwise, it is added to the external list. |
| * |
| * @param model model to be added to the entry |
| */ |
| public void addModel(IPluginModelBase model) { |
| if (model.getUnderlyingResource() != null) { |
| fWorkspaceEntries.add(model); |
| } else { |
| fExternalEntries.add(model); |
| } |
| } |
| |
| /** |
| * Removes the given model for the workspace list if the model is associated |
| * with workspace resource. Otherwise, it is removed from the external list. |
| * |
| * @param model model to be removed from the model entry |
| */ |
| public void removeModel(IPluginModelBase model) { |
| if (model.getUnderlyingResource() != null) { |
| fWorkspaceEntries.remove(model); |
| } else { |
| fExternalEntries.remove(model); |
| } |
| } |
| } |
| |
| private final ExternalModelManager fExternalManager; // keeps track of changes in target models |
| private final WorkspacePluginModelManager fWorkspaceManager; // keeps track of changes in the workspace |
| private PDEState fState; // keeps the combined view of the target and workspace |
| |
| private Map<String, LocalModelEntry> fEntries; // a master table keyed by plugin ID and the value is a ModelEntry |
| private ArrayList<IPluginModelListener> fListeners; // a list of listeners interested in changes to the plug-in models |
| private ArrayList<IStateDeltaListener> fStateListeners; // a list of listeners interested in changes to the PDE/resolver State |
| private boolean fCancelled = false; |
| |
| /** |
| * Initialize the workspace and external (target) model manager |
| * and add listeners to each one |
| */ |
| private PluginModelManager() { |
| fWorkspaceManager = new WorkspacePluginModelManager(); |
| fExternalManager = new ExternalModelManager(); |
| fExternalManager.addModelProviderListener(this); |
| fWorkspaceManager.addModelProviderListener(this); |
| } |
| |
| /** |
| * Provides the instance of {@link PluginModelManager}. If one doesn't exists already than a new one is created and |
| * the workspace and external (target) model manager are initialized with listeners added to each one |
| */ |
| public static synchronized PluginModelManager getInstance() { |
| if (fModelManager == null) { |
| fModelManager = new PluginModelManager(); |
| } |
| return fModelManager; |
| } |
| |
| /** |
| * Shuts down the instance of {@link PluginModelManager} if it exists. |
| */ |
| public static synchronized void shutdownInstance() { |
| if (fModelManager != null) { |
| fModelManager.shutdown(); |
| } |
| } |
| |
| /** |
| * React to changes in plug-ins in the workspace and/or target |
| */ |
| @Override |
| public void modelsChanged(IModelProviderEvent e) { |
| PluginModelDelta delta = new PluginModelDelta(); |
| |
| // Removes from the master table and the state all workspace plug-ins that have been |
| // removed (project closed/deleted) from the workspace. |
| // Also if the target location changes, all models from the old target are removed |
| if ((e.getEventTypes() & IModelProviderEvent.MODELS_REMOVED) != 0) { |
| IModel[] removed = e.getRemovedModels(); |
| for (IModel element : removed) { |
| IPluginModelBase model = (IPluginModelBase) element; |
| String id = model.getPluginBase().getId(); |
| if (id != null) { |
| handleRemove(id, model, delta); |
| } |
| } |
| } |
| |
| Set<String> addedBSNs = new HashSet<>(); |
| // Adds to the master table and the state newly created plug-ins in the workspace |
| // (ie. new plug-in project or a closed project that has just been re-opened). |
| // Also, if the target location changes, we add all plug-ins from the new target |
| if ((e.getEventTypes() & IModelProviderEvent.MODELS_ADDED) != 0) { |
| IModel[] added = e.getAddedModels(); |
| for (IModel element : added) { |
| IPluginModelBase model = (IPluginModelBase) element; |
| String id = model.getPluginBase().getId(); |
| if (id != null) { |
| handleAdd(id, model, delta); |
| addedBSNs.add(id); |
| } |
| } |
| } |
| |
| // Update the bundle description of plug-ins whose state has changed. |
| // A plug-in changes state if the MANIFEST.MF has been touched. |
| // or if a plug-in on the Target Platform has changed state (from checked to unchecked, |
| // and vice versa. |
| if ((e.getEventTypes() & IModelProviderEvent.MODELS_CHANGED) != 0) { |
| IModel[] changed = e.getChangedModels(); |
| for (IModel element : changed) { |
| handleChange((IPluginModelBase) element, delta); |
| } |
| } |
| |
| if (fState != null) { |
| // if the target location has not changed, incrementally re-resolve the state after processing all the add/remove/modify changes |
| // Otherwise, the state is in a good resolved state |
| StateDelta stateDelta = null; |
| if (addedBSNs.isEmpty()) { |
| // resolve incrementally |
| stateDelta = fState.resolveState(true); |
| } else { |
| // resolve based on added bundles, in case there are multiple versions of the added bundles |
| stateDelta = fState.resolveState(addedBSNs.toArray(new String[addedBSNs.size()])); |
| } |
| // trigger a classpath update for all workspace plug-ins affected by the |
| // processed batch of changes, run asynch for manifest changes |
| updateAffectedEntries(stateDelta, (e.getEventTypes() & IModelProviderEvent.MODELS_CHANGED) != 0); |
| fireStateDelta(stateDelta); |
| |
| } |
| |
| // notify all interested listeners in the changes made to the master table of entries |
| fireDelta(delta); |
| } |
| |
| /** |
| * Trigger a classpath update for all workspace plug-ins affected by the processed |
| * model changes |
| * |
| * @param delta a state delta containing a list of bundles affected by the processed |
| * changes, may be <code>null</code> to indicate the entire target has changed |
| * @param runAsynch whether classpath updates should be done in an asynchronous job |
| */ |
| private void updateAffectedEntries(StateDelta delta, boolean runAsynch) { |
| Map<IJavaProject, RequiredPluginsClasspathContainer> map = new HashMap<>(); |
| if (delta == null) { |
| // if the delta is null, then the entire target changed. |
| // Therefore, we should update the classpath for all workspace plug-ins. |
| IPluginModelBase[] models = getWorkspaceModels(); |
| for (IPluginModelBase model : models) { |
| IProject project = model.getUnderlyingResource().getProject(); |
| try { |
| if (project.hasNature(JavaCore.NATURE_ID)) { |
| map.put(JavaCore.create(project), new RequiredPluginsClasspathContainer(model)); |
| } |
| } catch (CoreException e) { |
| } |
| } |
| } else { |
| BundleDelta[] deltas = delta.getChanges(); |
| for (BundleDelta bundleDelta : deltas) { |
| try { |
| // update classpath for workspace plug-ins that are housed in a |
| // Java project hand have been affected by the processd model changes. |
| IPluginModelBase model = findModel(bundleDelta.getBundle()); |
| IResource resource = model == null ? null : model.getUnderlyingResource(); |
| if (resource != null) { |
| IProject project = resource.getProject(); |
| if (project.hasNature(JavaCore.NATURE_ID)) { |
| IJavaProject jProject = JavaCore.create(project); |
| if (!map.containsKey(jProject)) { |
| map.put(jProject, new RequiredPluginsClasspathContainer(model)); |
| } |
| } |
| } |
| } catch (CoreException e) { |
| } |
| } |
| // do secondary dependencies |
| IPluginModelBase[] models = getWorkspaceModels(); |
| for (IPluginModelBase model : models) { |
| IProject project = model.getUnderlyingResource().getProject(); |
| try { |
| if (!project.hasNature(JavaCore.NATURE_ID)) { |
| continue; |
| } |
| IJavaProject jProject = JavaCore.create(project); |
| if (map.containsKey(jProject)) { |
| continue; |
| } |
| IBuild build = ClasspathUtilCore.getBuild(model); |
| if (build != null && build.getEntry(IBuildEntry.SECONDARY_DEPENDENCIES) != null) { |
| map.put(jProject, new RequiredPluginsClasspathContainer(model, build)); |
| } |
| } catch (CoreException e) { |
| } |
| } |
| } |
| |
| if (!map.isEmpty()) { |
| // update class path for all affected workspace plug-ins in one operation |
| Iterator<Entry<IJavaProject, RequiredPluginsClasspathContainer>> iterator = map.entrySet().iterator(); |
| IJavaProject[] projects = new IJavaProject[map.size()]; |
| IClasspathContainer[] containers = new IClasspathContainer[projects.length]; |
| int index = 0; |
| while (iterator.hasNext()) { |
| Entry<IJavaProject, RequiredPluginsClasspathContainer> entry = iterator.next(); |
| projects[index] = entry.getKey(); |
| containers[index] = entry.getValue(); |
| index++; |
| } |
| // TODO Consider always running in a job - better reporting and cancellation options |
| if (runAsynch) { |
| // We may be in the UI thread, so the classpath is updated in a job to avoid blocking (bug 376135) |
| fUpdateJob.add(projects, containers); |
| fUpdateJob.schedule(); |
| } else { |
| // else update synchronously |
| try { |
| JavaCore.setClasspathContainer(PDECore.REQUIRED_PLUGINS_CONTAINER_PATH, projects, containers, null); |
| } catch (JavaModelException e) { |
| } |
| } |
| } |
| } |
| |
| /** |
| * Notify all interested listeners in changes made to the master table |
| * |
| * @param delta the delta of changes |
| */ |
| private void fireDelta(PluginModelDelta delta) { |
| if (fListeners != null) { |
| for (int i = 0; i < fListeners.size(); i++) { |
| fListeners.get(i).modelsChanged(delta); |
| } |
| } |
| } |
| |
| /** |
| * Notify all interested listeners in changes made to the resolver State |
| * |
| * @param delta the delta from the resolver State. |
| */ |
| private void fireStateDelta(StateDelta delta) { |
| if (fStateListeners != null) { |
| ListIterator<IStateDeltaListener> li = fStateListeners.listIterator(); |
| while (li.hasNext()) { |
| li.next().stateResolved(delta); |
| } |
| } |
| } |
| |
| /** |
| * Notify all interested listeners the cached PDEState has changed |
| * |
| * @param newState the new PDEState. |
| */ |
| private void fireStateChanged(PDEState newState) { |
| if (fStateListeners != null) { |
| ListIterator<IStateDeltaListener> li = fStateListeners.listIterator(); |
| while (li.hasNext()) { |
| li.next().stateChanged(newState.getState()); |
| } |
| } |
| } |
| |
| /** |
| * Add a listener to the model manager |
| * |
| * @param listener the listener to be added |
| */ |
| public void addPluginModelListener(IPluginModelListener listener) { |
| if (fListeners == null) { |
| fListeners = new ArrayList<>(); |
| } |
| if (!fListeners.contains(listener)) { |
| fListeners.add(listener); |
| } |
| } |
| |
| /** |
| * Add a StateDelta listener to model manager |
| * |
| * @param listener the listener to be added |
| */ |
| public void addStateDeltaListener(IStateDeltaListener listener) { |
| if (fStateListeners == null) { |
| fStateListeners = new ArrayList<>(); |
| } |
| if (!fStateListeners.contains(listener)) { |
| fStateListeners.add(listener); |
| } |
| } |
| |
| /** |
| * Remove a listener from the model manager |
| * |
| * @param listener the listener to be removed |
| */ |
| public void removePluginModelListener(IPluginModelListener listener) { |
| if (fListeners != null) { |
| fListeners.remove(listener); |
| } |
| } |
| |
| /** |
| * Remove a StateDelta listener from the model manager |
| * |
| * @param listener the listener to be removed |
| */ |
| public void removeStateDeltaListener(IStateDeltaListener listener) { |
| if (fStateListeners != null) { |
| fStateListeners.remove(listener); |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if neither the workspace nor target contains plug-ins; |
| * <code>false</code> otherwise. |
| * |
| * @return <code>true</code> if neither the workspace nor target contains plug-ins; |
| * <code>false</code> otherwise. |
| */ |
| public boolean isEmpty() { |
| return getEntryTable().isEmpty(); |
| } |
| |
| /** |
| * Returns <code>true</code> if the master table has been initialized; |
| * <code>false</code> otherwise. |
| * |
| * @return <code>true</code> if the master table has been initialized; |
| * <code>false</code> otherwise. |
| */ |
| public boolean isInitialized() { |
| return fEntries != null; |
| } |
| |
| /** |
| * Returns whether the model initialization was cancelled by the user. |
| * Other initializations, such as FeatureModelManager should use this |
| * setting to avoid resolving the target platform when the user has chosen |
| * to cancel it previously. |
| * |
| * @return <code>true</code> if the user cancelled the initialization the last time it was run; |
| * <code>false</code> otherwise |
| */ |
| public boolean isCancelled() { |
| return fCancelled; |
| } |
| |
| /** |
| * Clears all existing models and recreates them |
| */ |
| public void targetReloaded(IProgressMonitor monitor) { |
| fEntries = null; |
| initializeTable(monitor); |
| } |
| |
| /** |
| * Allow access to the table only through this getter. |
| * It always calls initialize to make sure the table is initialized. |
| * If more than one thread tries to read the table at the same time, |
| * and the table is not initialized yet, thread2 would wait. |
| * This way there are no partial reads. |
| */ |
| private Map<String, LocalModelEntry> getEntryTable() { |
| initializeTable(null); |
| return fEntries; |
| } |
| |
| /** |
| * |
| * This method must be synchronized so that only one thread |
| * initializes the table, and the rest would block until |
| * the table is initialized. |
| * |
| */ |
| private synchronized void initializeTable(IProgressMonitor monitor) { |
| if (fEntries != null) { |
| return; |
| } |
| |
| // Check if PlatformAdmin service is available (Bug 413450) |
| PlatformAdmin pAdmin = Platform.getPlatformAdmin(); |
| if (pAdmin == null) { |
| PDECore.logErrorMessage(PDECoreMessages.PluginModelManager_PlatformAdminMissingErrorMessage); |
| fEntries = Collections.emptyMap(); |
| return; |
| } |
| |
| SubMonitor subMon = SubMonitor.convert(monitor, PDECoreMessages.PluginModelManager_InitializingPluginModels, 100); |
| if (PDECore.DEBUG_MODEL) { |
| if (fState == null) { |
| System.out.println("\nInitializing PDE models"); //$NON-NLS-1$ |
| } else { |
| System.out.println("\nTarget changed, recreating PDE models"); //$NON-NLS-1$ |
| } |
| } |
| |
| PDEState oldState = fState; |
| long startTime = System.currentTimeMillis(); |
| |
| // Cannot assign to fEntries here - will create a race condition with isInitialized() |
| Map<String, LocalModelEntry> entries = Collections.synchronizedMap(new TreeMap<String, LocalModelEntry>()); |
| fCancelled = false; |
| |
| ITargetDefinition unresolvedRepoBasedtarget = null; |
| try { |
| unresolvedRepoBasedtarget = TargetPlatformHelper.getUnresolvedRepositoryBasedWorkspaceTarget(); |
| } catch (CoreException e) { |
| PDECore.log(e); |
| } |
| if (unresolvedRepoBasedtarget != null && !P2TargetUtils.isProfileValid(unresolvedRepoBasedtarget)) { |
| //Workspace target contains unresolved p2 repositories, |
| //set empty fState, fExternalManager, fEntries- scheduling target platform resolve |
| fState = new PDEState(new URI[0], true, true, subMon); |
| fExternalManager.setModels(new IPluginModelBase[0]); |
| fEntries = entries; |
| LoadTargetDefinitionJob.load(unresolvedRepoBasedtarget); |
| return; |
| } |
| |
| long startTargetModels = System.currentTimeMillis(); |
| // Target models |
| URI[] externalUris = getExternalBundles(subMon.split(40)); |
| if (subMon.isCanceled()) { |
| // If target resolution is cancelled, externalUrls will be empty. Log warning so user knows how to reload the target. |
| if (PDECore.DEBUG_MODEL) { |
| System.out.println("Target platform initialization cancelled by user"); //$NON-NLS-1$ |
| } |
| PDECore.log(Status.warning(PDECoreMessages.PluginModelManager_TargetInitCancelledLog)); |
| // Set a flag so the feature model manager can avoid starting the target resolve again |
| fCancelled = true; |
| } |
| |
| fState = new PDEState(externalUris, true, true, subMon.split(15)); |
| fExternalManager.setModels(fState.getTargetModels()); |
| addToTable(entries, fExternalManager.getAllModels()); |
| |
| // Check if the saved external bundle list has changed, if so target contents is different and projects should be rebuilt |
| boolean externalPluginsChanged = isSavedExternalPluginListDifferent(externalUris); |
| saveExternalPluginList(externalUris); |
| |
| if (PDECore.DEBUG_MODEL) { |
| System.out.println(fState.getTargetModels().length + " target models created in " + (System.currentTimeMillis() - startTargetModels) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| // Workspace models |
| IPluginModelBase[] models = fWorkspaceManager.getPluginModels(); |
| addToTable(entries, models); |
| long startWorkspaceAdditions = System.currentTimeMillis(); |
| // add workspace plug-ins to the state |
| // and remove their target counterparts from the state. |
| for (IPluginModelBase model : models) { |
| addWorkspaceBundleToState(entries, model); |
| } |
| subMon.split(15); |
| |
| if (PDECore.DEBUG_MODEL) { |
| System.out.println(fWorkspaceManager.getModelsMap().size() + " workspace models created in " //$NON-NLS-1$ |
| + (System.currentTimeMillis() - startWorkspaceAdditions) + " ms"); //$NON-NLS-1$ |
| } |
| |
| // Resolve the state for all external and workspace models |
| fState.resolveState(true); |
| subMon.split(5); |
| |
| fEntries = entries; |
| // flush the extension registry cache since workspace data (BundleDescription id's) have changed. |
| PDECore.getDefault().getExtensionsRegistry().targetReloaded(); |
| if (oldState != null) { |
| // Need to update classpath entries |
| updateAffectedEntries(null, true); |
| } |
| |
| // Fire a state change event to touch all projects if the target content has changed since last model init |
| if (externalPluginsChanged) { |
| fireStateChanged(fState); |
| if (PDECore.DEBUG_MODEL) { |
| System.out.println("Loaded target models differ from saved list, PDE builder will run on all projects."); //$NON-NLS-1$ |
| } |
| } |
| |
| subMon.split(25); |
| if (PDECore.DEBUG_MODEL) { |
| long time = System.currentTimeMillis() - startTime; |
| System.out.println("PDE plug-in model initialization complete: " + time + " ms"); //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| |
| } |
| |
| /** |
| * Returns an array of URI plug-in locations for external bundles loaded from the |
| * current target platform. |
| * |
| * @param monitor progress monitor |
| * @return array of URLs for external bundles |
| */ |
| private URI[] getExternalBundles(IProgressMonitor monitor) { |
| ITargetDefinition target = null; |
| try { |
| target = TargetPlatformHelper.getWorkspaceTargetResolved(monitor); |
| } catch (CoreException e) { |
| PDECore.log(e); |
| return new URI[0]; |
| } |
| |
| // Resolution was cancelled |
| if (target == null) { |
| return new URI[0]; |
| } |
| |
| // Log any known issues with the target platform to warn user |
| if (target.isResolved()) { |
| if (target.getStatus().getSeverity() == IStatus.ERROR) { |
| PDECore.log(Status.error(PDECoreMessages.PluginModelManager_CurrentTargetPlatformContainsErrors, new CoreException(target.getStatus()))); |
| if (target.getStatus() instanceof MultiStatus) { |
| MultiStatus multiStatus = (MultiStatus) target.getStatus(); |
| for (IStatus childStatus : multiStatus.getChildren()) { |
| PDECore.log(childStatus); |
| } |
| } |
| } |
| } |
| |
| URI[] externalURIs = new URI[0]; |
| TargetBundle[] bundles = target.getBundles(); |
| if (bundles != null) { |
| List<URI> uris = new ArrayList<>(bundles.length); |
| for (TargetBundle bundle : bundles) { |
| if (bundle.getStatus().isOK()) { |
| uris.add(bundle.getBundleInfo().getLocation()); |
| } |
| } |
| externalURIs = uris.toArray(new URI[uris.size()]); |
| } |
| |
| return externalURIs; |
| |
| } |
| |
| /** |
| * Adds the given models to the corresponding ModelEntry in the master table |
| * |
| * @param models the models to be added to the master table |
| */ |
| private void addToTable(Map<String, LocalModelEntry> entries, IPluginModelBase[] models) { |
| for (IPluginModelBase model : models) { |
| String id = model.getPluginBase().getId(); |
| if (id == null) { |
| continue; |
| } |
| LocalModelEntry entry = entries.get(id); |
| // create a new entry for the given ID if none already exists |
| if (entry == null) { |
| entry = new LocalModelEntry(id); |
| entries.put(id, entry); |
| } |
| // add the model to the entry |
| entry.addModel(model); |
| } |
| } |
| |
| /** |
| * Add a workspace bundle to the state |
| * |
| * @param model the workspace model |
| */ |
| private synchronized void addWorkspaceBundleToState(IPluginModelBase model) { |
| addWorkspaceBundleToState(fEntries, model); |
| } |
| |
| private synchronized void addWorkspaceBundleToState(Map<String, LocalModelEntry> entries, IPluginModelBase model) { |
| String id = model.getPluginBase().getId(); |
| if (id == null) { |
| return; |
| } |
| |
| // update target models by the same ID from the state, if any |
| PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager(); |
| boolean preferWorkspaceBundle = prefs.getBoolean(ICoreConstants.WORKSPACE_PLUGINS_OVERRIDE_TARGET); |
| ModelEntry entry = entries.get(id); |
| if (entry != null) { |
| for (IPluginModelBase externalModel : entry.getExternalModels()) { |
| if (preferWorkspaceBundle) { |
| fState.removeBundleDescription(externalModel.getBundleDescription()); |
| } else { |
| fState.updateBundleDescription(externalModel.getBundleDescription()); |
| } |
| } |
| } |
| |
| // add new bundle to the state |
| fState.addBundle(model, false); |
| |
| BundleDescription desc = model.getBundleDescription(); |
| if (desc != null) { |
| // refresh host if a fragment is added to the state. |
| // this is necessary because the state will not re-resolve dynamically added fragments |
| // on its own |
| HostSpecification spec = desc.getHost(); |
| if (spec != null && ("true".equals(System.getProperty("pde.allowCycles")) //$NON-NLS-1$ //$NON-NLS-2$ |
| || isPatchFragment(entries, desc) || desc.getImportPackages().length > 0 || desc.getRequiredBundles().length > 0)) { |
| BundleDescription host = (BundleDescription) spec.getSupplier(); |
| if (host != null) { |
| ModelEntry hostEntry = entries.get(host.getName()); |
| if (hostEntry != null) { |
| fState.addBundle(hostEntry.getModel(host), true); |
| } |
| } |
| } |
| } |
| } |
| |
| // Cannot directly call ClasspathUtilCore.isPatchFragment(BundleDescription) since it would cause a loop in our initialization. |
| private boolean isPatchFragment(Map<String, LocalModelEntry> entries, BundleDescription desc) { |
| ModelEntry entry = entries.get(desc.getSymbolicName()); |
| if (entry != null) { |
| IPluginModelBase base = entry.getModel(desc); |
| if (base == null) { |
| return false; |
| } |
| return ClasspathUtilCore.isPatchFragment(base); |
| } |
| return false; |
| } |
| |
| /** |
| * Saves the given list of external plugin uris to a file in the metadata folder |
| * @param uris url list to save |
| */ |
| private void saveExternalPluginList(URI[] uris) { |
| File dir = new File(PDECore.getDefault().getStateLocation().toOSString()); |
| File saveLocation = new File(dir, fExternalPluginListFile); |
| try (FileWriter fileWriter = new FileWriter(saveLocation, false)) { |
| fileWriter.write("# List of external plug-in models previously loaded. Timestamp: " + System.currentTimeMillis() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ |
| for (URI uri : uris) { |
| fileWriter.write(uri.toString()); |
| fileWriter.write("\n"); //$NON-NLS-1$ |
| } |
| fileWriter.flush(); |
| } catch (IOException e) { |
| PDECore.log(e); |
| } |
| } |
| |
| /** |
| * Returns whether the saved list of external plugins is different from the given list of external plugins. |
| * |
| * @param newUris the uris to compare against the saved list |
| * @return <code>true</code> if the two plug-in lists differ, <code>false</code> if they match |
| */ |
| private boolean isSavedExternalPluginListDifferent(URI[] newUris) { |
| Set<String> newExternal = new LinkedHashSet<>(); |
| for (URI newUri : newUris) { |
| newExternal.add(newUri.toString()); |
| } |
| |
| File dir = new File(PDECore.getDefault().getStateLocation().toOSString()); |
| File saveLocation = new File(dir, fExternalPluginListFile); |
| |
| if (!saveLocation.exists()) { |
| // If the external list has never been saved, but the target platform has nothing in it, there is no need to build |
| return !newExternal.isEmpty(); |
| } |
| |
| Set<String> previousExternal = new LinkedHashSet<>(); |
| try (BufferedReader reader = new BufferedReader(new FileReader(saveLocation));) { |
| while (reader.ready()) { |
| String url = reader.readLine(); |
| if (url != null && !url.trim().isEmpty() && !url.startsWith("#")) { //$NON-NLS-1$ |
| previousExternal.add(url.trim()); |
| } |
| } |
| if (previousExternal.size() != newExternal.size()) { |
| return true; |
| } |
| Iterator<String> iter = previousExternal.iterator(); |
| while (iter.hasNext()) { |
| if (!newExternal.remove(iter.next())) { |
| return true; |
| } |
| } |
| if (!newExternal.isEmpty()) { |
| return true; |
| } |
| } catch (IOException e) { |
| PDECore.log(e); |
| } |
| return false; |
| } |
| |
| /** |
| * Adds a model to the master table and state |
| * |
| * @param id the key |
| * @param model the model being added |
| */ |
| private void handleAdd(String id, IPluginModelBase model, PluginModelDelta delta) { |
| LocalModelEntry entry = getEntryTable().get(id); |
| |
| // add model to the corresponding ModelEntry. Create a new entry if necessary |
| if (entry == null) { |
| entry = new LocalModelEntry(id); |
| getEntryTable().put(id, entry); |
| delta.addEntry(entry, PluginModelDelta.ADDED); |
| } else { |
| delta.addEntry(entry, PluginModelDelta.CHANGED); |
| } |
| entry.addModel(model); |
| |
| // if the model added is a workspace model, add it to the state and |
| // remove all its external counterparts |
| if (model.getUnderlyingResource() != null) { |
| addWorkspaceBundleToState(model); |
| } else if (model.isEnabled() && !entry.hasWorkspaceModels()) { |
| // if a target model has went from an unchecked state to a checked state |
| // on the target platform preference page, re-add its bundle description |
| // to the state |
| BundleDescription desc = model.getBundleDescription(); |
| if (desc.getContainingState().equals(fState.fState)) { |
| fState.addBundleDescription(desc); |
| } |
| } |
| } |
| |
| /** |
| * Removes the model from the ModelEntry and the state. The entire model entry is removed |
| * once the last model it retains is removed. |
| * |
| * @param id the key |
| * @param model the model to be removed |
| */ |
| private void handleRemove(String id, IPluginModelBase model, PluginModelDelta delta) { |
| LocalModelEntry entry = getEntryTable().get(id); |
| if (entry != null) { |
| // remove model from the entry |
| entry.removeModel(model); |
| // remove corresponding bundle description from the state |
| fState.removeBundleDescription(model.getBundleDescription()); |
| if (!entry.hasExternalModels() && !entry.hasWorkspaceModels()) { |
| // remove entire entry if it has no models left |
| getEntryTable().remove(id); |
| delta.addEntry(entry, PluginModelDelta.REMOVED); |
| return; |
| } else if (model.getUnderlyingResource() != null && !entry.hasWorkspaceModels()) { |
| // re-add enabled external counterparts to the state, if the last workspace |
| // plug-in with a particular symbolic name is removed |
| IPluginModelBase[] external = entry.getExternalModels(); |
| for (IPluginModelBase element : external) { |
| if (element.isEnabled()) { |
| fState.addBundleDescription(element.getBundleDescription()); |
| } |
| } |
| } |
| delta.addEntry(entry, PluginModelDelta.CHANGED); |
| } |
| } |
| |
| /** |
| * Update the state and master table to account for the change in the given model |
| * |
| * @param model the model that has changed |
| */ |
| private void handleChange(IPluginModelBase model, PluginModelDelta delta) { |
| BundleDescription desc = model.getBundleDescription(); |
| String oldID = desc == null ? null : desc.getSymbolicName(); |
| String newID = model.getPluginBase().getId(); |
| |
| // if the model still has no symbolic name (ie. a MANIFEST.MF without the |
| // Bundle-SymbolicName header), keep ignoring it |
| if (oldID == null && newID == null) { |
| return; |
| } |
| |
| // if the model used to lack a Bundle-SymbolicName header and now it has one, |
| // treat it as a regular model addition |
| if (oldID == null && newID != null) { |
| handleAdd(newID, model, delta); |
| } else if (oldID != null && newID == null) { |
| // if the model used to have a Bundle-SymbolicName header and now it lost it, |
| // treat it as a regular model removal |
| handleRemove(oldID, model, delta); |
| model.setBundleDescription(null); |
| } else if (oldID != null && oldID.equals(newID)) { |
| // if the workspace bundle's MANIFEST.MF was touched or |
| // if the a target plug-in has now become enabled/checked, update the model |
| // in the state |
| if (model.isEnabled()) { |
| // if the state of an inactive bundle changes (external model un/checked that has an |
| // equivalent workspace bundle), then take no action. We don't want to add the external |
| // model to the state when it is enabled if we have a workspace bundle already in the state. |
| ModelEntry entry = getEntryTable().get(oldID); |
| IPluginModelBase[] activeModels = entry.getActiveModels(); |
| boolean isActive = false; |
| for (IPluginModelBase activeModel : activeModels) { |
| if (activeModel == model) { |
| isActive = true; |
| break; |
| } |
| } |
| if (isActive) { |
| // refresh everything related to this bundle model id |
| fEntries.remove(newID); |
| fState.removeBundleDescription(desc); |
| for (int i = 0; i < fExternalManager.getAllModels().length; i++) { |
| IPluginModelBase modelExternal = fExternalManager.getAllModels()[i]; |
| if (modelExternal.getPluginBase().getId() != null) { |
| if (modelExternal.getPluginBase().getId().equals(newID)) { |
| addToTable(fEntries, new IPluginModelBase[] { modelExternal }); |
| } |
| } |
| } |
| IPluginModelBase[] models = fWorkspaceManager.getPluginModels(); |
| for (IPluginModelBase modelWorkspace : models) { |
| if (modelWorkspace.getPluginBase().getId() != null) { |
| if (modelWorkspace.getPluginBase().getId().equals(newID)) { |
| addToTable(fEntries, new IPluginModelBase[] { modelWorkspace }); |
| addWorkspaceBundleToState(fEntries, modelWorkspace); |
| } |
| } |
| } |
| } |
| } else { |
| // if the target plug-in has become disabled/unchecked, remove its bundle |
| // description from the state |
| fState.removeBundleDescription(model.getBundleDescription()); |
| } |
| delta.addEntry(findEntry(oldID), PluginModelDelta.CHANGED); |
| } else { |
| // if the symbolic name of the bundle has completely changed, |
| // remove the model from the old entry, and add the model to the new entry |
| handleRemove(oldID, model, delta); |
| handleAdd(newID, model, delta); |
| } |
| } |
| |
| /** |
| * Returns a model entry containing all workspace and target plug-ins by the given ID |
| * |
| * @param id the plug-in ID |
| * |
| * @return a model entry containing all workspace and target plug-ins by the given ID |
| */ |
| public ModelEntry findEntry(String id) { |
| if ("system.bundle".equals(id)) { //$NON-NLS-1$ |
| id = getSystemBundleId(); |
| } |
| return id == null ? null : (ModelEntry) getEntryTable().get(id); |
| } |
| |
| /** |
| * Returns the plug-in model for the best match plug-in with the given ID. |
| * A null value is returned if no such bundle is found in the workspace or target platform. |
| * <p> |
| * A workspace plug-in is always preferably returned over a target plug-in. |
| * A plug-in that is checked/enabled on the Target Platform preference page is always |
| * preferably returned over a target plug-in that is unchecked/disabled. |
| * </p> |
| * <p> |
| * In the case of a tie among workspace plug-ins or among target plug-ins, |
| * the plug-in with the highest version is returned. |
| * </p> |
| * <p> |
| * In the case of a tie among more than one suitable plug-in that have the same version, |
| * one of those plug-ins is randomly returned. |
| * </p> |
| * |
| * @param id the plug-in ID |
| * @return the plug-in model for the best match plug-in with the given ID |
| */ |
| public IPluginModelBase findModel(String id) { |
| ModelEntry entry = findEntry(id); |
| return entry == null ? null : entry.getModel(); |
| } |
| |
| /** |
| * Returns the plug-in model corresponding to the given project, or <code>null</code> |
| * if the project does not represent a plug-in project or if it contains a manifest file |
| * that is malformed or missing vital information. |
| * |
| * @param project the project |
| * @return a plug-in model corresponding to the project or <code>null</code> if the project |
| * is not a plug-in project |
| */ |
| public IPluginModelBase findModel(IProject project) { |
| initializeTable(null); |
| return fWorkspaceManager.getModel(project); |
| } |
| |
| /** |
| * Returns a plug-in model associated with the given bundle description |
| * |
| * @param desc the bundle description |
| * |
| * @return a plug-in model associated with the given bundle description or <code>null</code> |
| * if none exists |
| */ |
| public IPluginModelBase findModel(BundleDescription desc) { |
| ModelEntry entry = (desc != null) ? findEntry(desc.getSymbolicName()) : null; |
| return entry == null ? null : entry.getModel(desc); |
| } |
| |
| /** |
| * Returns all plug-ins and fragments in the workspace as well as all plug-ins and fragments that are |
| * checked on the Target Platform preference page. |
| * <p> |
| * If a workspace plug-in/fragment has the same ID as a target plug-in/fragment, the target counterpart |
| * is skipped and not included. |
| * </p> |
| * <p> |
| * Equivalent to <code>getActiveModels(true)</code> |
| * </p> |
| * |
| * @return all plug-ins and fragments in the workspace as well as all plug-ins and fragments that are |
| * checked on the Target Platform preference page. |
| */ |
| public IPluginModelBase[] getActiveModels() { |
| return getActiveModels(true); |
| } |
| |
| /** |
| * Returns all plug-ins and (possibly) fragments in the workspace as well as all plug-ins and (possibly) |
| * fragments that are checked on the Target Platform preference page. |
| * <p> |
| * If a workspace plug-in/fragment has the same ID as a target plug-in, the target counterpart |
| * is skipped and not included. |
| * </p> |
| * <p> |
| * The returned result includes fragments only if <code>includeFragments</code> |
| * is set to true |
| * </p> |
| * @param includeFragments a boolean indicating if fragments are desired in the returned |
| * result |
| * @return all plug-ins and (possibly) fragments in the workspace as well as all plug-ins and |
| * (possibly) fragments that are checked on the Target Platform preference page. |
| */ |
| public IPluginModelBase[] getActiveModels(boolean includeFragments) { |
| int size = getEntryTable().size(); |
| ArrayList<IPluginModelBase> result = new ArrayList<>(size); |
| Iterator<LocalModelEntry> iter = getEntryTable().values().iterator(); |
| while (iter.hasNext()) { |
| ModelEntry entry = iter.next(); |
| IPluginModelBase[] models = entry.getActiveModels(); |
| for (IPluginModelBase model : models) { |
| if (model instanceof IPluginModel || includeFragments) { |
| result.add(model); |
| } |
| } |
| } |
| return result.toArray(new IPluginModelBase[result.size()]); |
| } |
| |
| /** |
| * Returns all plug-ins and fragments in the workspace as well as all target plug-ins and fragments, regardless |
| * whether or not they are checked or not on the Target Platform preference page. |
| * <p> |
| * If a workspace plug-in/fragment has the same ID as a target plug-in, the target counterpart |
| * is skipped and not included. |
| * </p> |
| * <p> |
| * Equivalent to <code>getAllModels(true)</code> |
| * </p> |
| * |
| * @return all plug-ins and fragments in the workspace as well as all target plug-ins and fragments, regardless |
| * whether or not they are checked on the Target Platform preference page. |
| */ |
| public IPluginModelBase[] getAllModels() { |
| return getAllModels(true); |
| } |
| |
| /** |
| * Returns all plug-ins and (possibly) fragments in the workspace as well as all plug-ins |
| * and (possibly) fragments, regardless whether or not they are |
| * checked on the Target Platform preference page. |
| * <p> |
| * If a workspace plug-in/fragment has the same ID as a target plug-in/fragment, the target counterpart |
| * is skipped and not included. |
| * </p> |
| * <p> |
| * The returned result includes fragments only if <code>includeFragments</code> |
| * is set to true |
| * </p> |
| * @param includeFragments a boolean indicating if fragments are desired in the returned |
| * result |
| * @return ll plug-ins and (possibly) fragments in the workspace as well as all plug-ins |
| * and (possibly) fragments, regardless whether or not they are |
| * checked on the Target Platform preference page. |
| */ |
| public IPluginModelBase[] getAllModels(boolean includeFragments) { |
| int size = getEntryTable().size(); |
| ArrayList<IPluginModelBase> result = new ArrayList<>(size); |
| Iterator<LocalModelEntry> iter = getEntryTable().values().iterator(); |
| while (iter.hasNext()) { |
| ModelEntry entry = iter.next(); |
| IPluginModelBase[] models = entry.hasWorkspaceModels() ? entry.getWorkspaceModels() : entry.getExternalModels(); |
| for (IPluginModelBase model : models) { |
| if (model instanceof IPluginModel || includeFragments) { |
| result.add(model); |
| } |
| } |
| } |
| return result.toArray(new IPluginModelBase[result.size()]); |
| } |
| |
| /** |
| * Returns all plug-in models in the target platform |
| * |
| * @return all plug-ins in the target platform |
| */ |
| public IPluginModelBase[] getExternalModels() { |
| initializeTable(null); |
| return fExternalManager.getAllModels(); |
| } |
| |
| /** |
| * Returns all plug-in models in the workspace |
| * |
| * @return all plug-in models in the workspace |
| */ |
| public IPluginModelBase[] getWorkspaceModels() { |
| initializeTable(null); |
| return fWorkspaceManager.getPluginModels(); |
| } |
| |
| /** |
| * Return the model manager that keeps track of plug-ins in the target platform |
| * |
| * @return the model manager that keeps track of plug-ins in the target platform |
| */ |
| public ExternalModelManager getExternalModelManager() { |
| initializeTable(null); |
| return fExternalManager; |
| } |
| |
| /** |
| * Returns the state containing bundle descriptions for workspace plug-ins and target plug-ins |
| * that form the current PDE state |
| */ |
| public PDEState getState() { |
| initializeTable(null); |
| return fState; |
| } |
| |
| /** |
| * Returns the id of the system bundle currently in the resolver state |
| * |
| * @return a String with the id of the system.bundle |
| */ |
| public String getSystemBundleId() { |
| return getState().getSystemBundle(); |
| } |
| |
| /** |
| * Perform cleanup upon shutting down |
| */ |
| protected void shutdown() { |
| fWorkspaceManager.shutdown(); |
| fExternalManager.shutdown(); |
| |
| if (fListeners != null) { |
| fListeners.clear(); |
| } |
| if (fStateListeners != null) { |
| fStateListeners.clear(); |
| } |
| } |
| |
| public void addExtensionDeltaListener(IExtensionDeltaListener listener) { |
| fWorkspaceManager.addExtensionDeltaListener(listener); |
| } |
| |
| public void removeExtensionDeltaListener(IExtensionDeltaListener listener) { |
| fWorkspaceManager.removeExtensionDeltaListener(listener); |
| } |
| } |