blob: 8cd46e90526bee68273ebb852993b7c7fd2a120d [file] [log] [blame]
/*******************************************************************************
* 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);
}
}