| /******************************************************************************* |
| * Copyright (c) 2006, 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 |
| *******************************************************************************/ |
| package org.eclipse.pde.internal.core; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.pde.core.IModel; |
| import org.eclipse.pde.core.IModelProviderEvent; |
| import org.eclipse.pde.core.plugin.IPluginModelBase; |
| import org.eclipse.pde.core.plugin.ISharedExtensionsModel; |
| import org.eclipse.pde.internal.build.IPDEBuildConstants; |
| import org.eclipse.pde.internal.core.builders.SchemaTransformer; |
| import org.eclipse.pde.internal.core.bundle.BundleFragmentModel; |
| import org.eclipse.pde.internal.core.bundle.BundlePluginModel; |
| import org.eclipse.pde.internal.core.bundle.WorkspaceBundleModel; |
| import org.eclipse.pde.internal.core.ibundle.IBundleModel; |
| import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase; |
| import org.eclipse.pde.internal.core.ibundle.IManifestHeader; |
| import org.eclipse.pde.internal.core.ischema.ISchema; |
| import org.eclipse.pde.internal.core.ischema.ISchemaDescriptor; |
| import org.eclipse.pde.internal.core.plugin.WorkspaceExtensionsModel; |
| import org.eclipse.pde.internal.core.plugin.WorkspaceFragmentModel; |
| import org.eclipse.pde.internal.core.plugin.WorkspacePluginModel; |
| import org.eclipse.pde.internal.core.project.PDEProject; |
| import org.eclipse.pde.internal.core.schema.SchemaDescriptor; |
| import org.osgi.framework.Constants; |
| |
| public class WorkspacePluginModelManager extends WorkspaceModelManager<IPluginModelBase> { |
| |
| @SuppressWarnings("deprecation") |
| private static final Collection<String> RELEVANT_HEADERS = Collections |
| .unmodifiableCollection(new HashSet<>(Arrays.asList( // |
| Constants.BUNDLE_MANIFESTVERSION, // |
| Constants.BUNDLE_SYMBOLICNAME, // |
| ICoreConstants.AUTOMATIC_MODULE_NAME, // |
| Constants.BUNDLE_VERSION, // |
| Constants.FRAGMENT_HOST, // |
| IPDEBuildConstants.EXTENSIBLE_API, // |
| IPDEBuildConstants.PATCH_FRAGMENT, // |
| Constants.REQUIRE_BUNDLE, // |
| Constants.IMPORT_PACKAGE, // |
| Constants.EXPORT_PACKAGE, // |
| ICoreConstants.PROVIDE_PACKAGE, // |
| ICoreConstants.ECLIPSE_JREBUNDLE, // |
| Constants.BUNDLE_CLASSPATH, // |
| Constants.PROVIDE_CAPABILITY, // |
| Constants.REQUIRE_CAPABILITY, // |
| ICoreConstants.ECLIPSE_GENERIC_CAPABILITY, // |
| ICoreConstants.ECLIPSE_GENERIC_REQUIRED, // |
| Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT, // |
| IPDEBuildConstants.ECLIPSE_PLATFORM_FILTER, // |
| ICoreConstants.ECLIPSE_SYSTEM_BUNDLE, // |
| ICoreConstants.ECLIPSE_SOURCE_BUNDLE))); |
| |
| private final ArrayList<IExtensionDeltaListener> fExtensionListeners = new ArrayList<>(); |
| private ArrayList<ModelChange> fChangedExtensions = null; |
| |
| /** |
| * The workspace plug-in model manager is only interested |
| * in changes to plug-in projects. |
| */ |
| @Override |
| protected boolean isInterestingProject(IProject project) { |
| return isPluginProject(project); |
| } |
| |
| /** |
| * Creates a plug-in model based on the project structure. |
| * <p> |
| * A bundle model is created if the project has a MANIFEST.MF file and optionally |
| * a plugin.xml/fragment.xml file. |
| * </p> |
| * <p> |
| * An old-style plugin model is created if the project only has a plugin.xml/fragment.xml |
| * file. |
| * </p> |
| */ |
| @Override |
| protected void createModel(IProject project, boolean notify) { |
| IPluginModelBase model = null; |
| IFile manifest = PDEProject.getManifest(project); |
| IFile pluginXml = PDEProject.getPluginXml(project); |
| IFile fragmentXml = PDEProject.getFragmentXml(project); |
| if (manifest.exists()) { |
| WorkspaceBundleModel bmodel = new WorkspaceBundleModel(manifest); |
| loadModel(bmodel, false); |
| if (bmodel.isFragmentModel()) { |
| model = new BundleFragmentModel(); |
| } else { |
| model = new BundlePluginModel(); |
| } |
| model.setEnabled(true); |
| bmodel.setEditable(false); |
| ((IBundlePluginModelBase) model).setBundleModel(bmodel); |
| |
| IFile efile = bmodel.isFragmentModel() ? fragmentXml : pluginXml; |
| if (efile.exists()) { |
| WorkspaceExtensionsModel extModel = new WorkspaceExtensionsModel(efile); |
| extModel.setEditable(false); |
| loadModel(extModel, false); |
| ((IBundlePluginModelBase) model).setExtensionsModel(extModel); |
| extModel.setBundleModel((IBundlePluginModelBase) model); |
| } |
| |
| } else if (pluginXml.exists()) { |
| model = new WorkspacePluginModel(pluginXml, true); |
| loadModel(model, false); |
| } else if (fragmentXml.exists()) { |
| model = new WorkspaceFragmentModel(fragmentXml, true); |
| loadModel(model, false); |
| } |
| |
| if (PDEProject.getOptionsFile(project).exists()) { |
| PDECore.getDefault().getTracingOptionsManager().reset(); |
| } |
| |
| if (model != null) { |
| getModelsMap().put(project, model); |
| if (notify) { |
| addChange(model, IModelProviderEvent.MODELS_ADDED); |
| } |
| } |
| } |
| |
| /** |
| * Reacts to changes in files of interest to PDE |
| */ |
| @Override |
| protected void handleFileDelta(IResourceDelta delta) { |
| IFile file = (IFile) delta.getResource(); |
| IProject project = file.getProject(); |
| String filename = file.getName(); |
| if (file.equals(PDEProject.getOptionsFile(project))) { |
| PDECore.getDefault().getTracingOptionsManager().reset(); |
| } else if (file.equals(PDEProject.getBuildProperties(project))) { |
| // change in build.properties should trigger a Classpath Update |
| // we therefore fire a notification |
| //TODO this is inefficient. we could do better. |
| IPluginModelBase model = getModel(project); |
| if (model != null) { |
| addChange(model, IModelProviderEvent.MODELS_CHANGED); |
| } |
| } else if (file.equals(PDEProject.getLocalizationFile(project))) { |
| // reset bundle resource if localization file has changed. |
| IPluginModelBase model = getModel(project); |
| if (model != null) { |
| ((AbstractNLModel) model).resetNLResourceHelper(); |
| } |
| } else if (filename.endsWith(".exsd")) { //$NON-NLS-1$ |
| handleEclipseSchemaDelta(file, delta); |
| } else { |
| if (file.equals(PDEProject.getPluginXml(project)) || file.equals(PDEProject.getFragmentXml(project))) { |
| handleExtensionFileDelta(file, delta); |
| } else if (file.equals(PDEProject.getManifest(project))) { |
| handleBundleManifestDelta(file, delta); |
| } |
| } |
| } |
| |
| /** |
| * @param schemaFile |
| * @param delta |
| */ |
| private void handleEclipseSchemaDelta(IFile schemaFile, IResourceDelta delta) { |
| // Get the kind of resource delta |
| int kind = delta.getKind(); |
| // We are only interested in schema files whose contents have changed |
| if (kind != IResourceDelta.CHANGED) { |
| return; |
| } else if ((IResourceDelta.CONTENT & delta.getFlags()) == 0) { |
| return; |
| } |
| // Get the schema preview file session property |
| Object property = null; |
| try { |
| property = schemaFile.getSessionProperty(PDECore.SCHEMA_PREVIEW_FILE); |
| } catch (CoreException e) { |
| // Ignore |
| return; |
| } |
| // Check if the schema file has an associated HTML schema preview file |
| // (That is, whether a show description action has been executed before) |
| // Property set in |
| // org.eclipse.pde.internal.ui.search.ShowDescriptionAction.linkPreviewFileToSchemaFile() |
| if (property == null) { |
| return; |
| } else if ((property instanceof File) == false) { |
| return; |
| } |
| File schemaPreviewFile = (File) property; |
| // Ensure the file exists and is writable |
| if (schemaPreviewFile.exists() == false) { |
| return; |
| } else if (schemaPreviewFile.isFile() == false) { |
| return; |
| } else if (schemaPreviewFile.canWrite() == false) { |
| return; |
| } |
| // Get the schema model object |
| ISchemaDescriptor descriptor = new SchemaDescriptor(schemaFile, false); |
| ISchema schema = descriptor.getSchema(false); |
| |
| try { |
| // Re-generate the schema preview file contents in order to reflect |
| // the changes in the schema |
| recreateSchemaPreviewFileContents(schemaPreviewFile, schema); |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| |
| /** |
| * @param schemaPreviewFile |
| * @param schema |
| * @throws IOException |
| */ |
| private void recreateSchemaPreviewFileContents(File schemaPreviewFile, ISchema schema) throws IOException { |
| SchemaTransformer transformer = new SchemaTransformer(); |
| try (OutputStream os = new FileOutputStream(schemaPreviewFile)) { |
| PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8), true); |
| transformer.transform(schema, printWriter); |
| os.flush(); |
| } |
| } |
| |
| /** |
| * Reacts to changes in the plugin.xml or fragment.xml file. |
| * <ul> |
| * <li>If the file has been deleted and the project has a MANIFEST.MF file, |
| * then this deletion only affects extensions and extension points.</li> |
| * <li>If the file has been deleted and the project does not have a MANIFEST.MF file, |
| * then it's an old-style plug-in and the entire model must be removed from the table.</li> |
| * <li>If the file has been added and the project already has a MANIFEST.MF, then |
| * this file only contributes extensions and extensions. No need to send a notification |
| * to trigger update classpath of dependent plug-ins</li> |
| * <li>If the file has been added and the project does not have a MANIFEST.MF, then |
| * an old-style plug-in has been created.</li> |
| * <li>If the file has been modified and the project already has a MANIFEST.MF, |
| * then reload the extensions model but do not send out notifications</li> |
| * </li>If the file has been modified and the project has no MANIFEST.MF, then |
| * it's an old-style plug-in, reload and send out notifications to trigger a classpath update |
| * for dependent plug-ins</li> |
| * </ul> |
| * @param file the manifest file |
| * @param delta the resource delta |
| */ |
| private void handleExtensionFileDelta(IFile file, IResourceDelta delta) { |
| int kind = delta.getKind(); |
| IPluginModelBase model = getModel(file.getProject()); |
| if (kind == IResourceDelta.REMOVED) { |
| if (model instanceof IBundlePluginModelBase) { |
| ((IBundlePluginModelBase) model).setExtensionsModel(null); |
| addExtensionChange(model, IModelProviderEvent.MODELS_REMOVED); |
| } else { |
| removeModel(file.getProject()); |
| } |
| } else if (kind == IResourceDelta.ADDED) { |
| if (model instanceof IBundlePluginModelBase) { |
| WorkspaceExtensionsModel extensions = new WorkspaceExtensionsModel(file); |
| extensions.setEditable(false); |
| ((IBundlePluginModelBase) model).setExtensionsModel(extensions); |
| extensions.setBundleModel((IBundlePluginModelBase) model); |
| loadModel(extensions, false); |
| addExtensionChange(model, IModelProviderEvent.MODELS_ADDED); |
| } else { |
| createModel(file.getProject(), true); |
| } |
| } else if (kind == IResourceDelta.CHANGED && (IResourceDelta.CONTENT & delta.getFlags()) != 0) { |
| if (model instanceof IBundlePluginModelBase) { |
| ISharedExtensionsModel extensions = ((IBundlePluginModelBase) model).getExtensionsModel(); |
| boolean reload = extensions != null; |
| if (extensions == null) { |
| extensions = new WorkspaceExtensionsModel(file); |
| ((WorkspaceExtensionsModel) extensions).setEditable(false); |
| ((IBundlePluginModelBase) model).setExtensionsModel(extensions); |
| ((WorkspaceExtensionsModel) extensions).setBundleModel((IBundlePluginModelBase) model); |
| } |
| loadModel(extensions, reload); |
| } else if (model != null) { |
| loadModel(model, true); |
| addChange(model, IModelProviderEvent.MODELS_CHANGED); |
| } |
| addExtensionChange(model, IModelProviderEvent.MODELS_CHANGED); |
| } |
| } |
| |
| /** |
| * Reacts to changes in the MANIFEST.MF file. |
| * <ul> |
| * <li>If the file has been deleted, switch to the old-style plug-in if a |
| * plugin.xml file exists</li> |
| * <li>If the file has been added, create a new bundle model</li> |
| * <li>If the file has been modified, reload the model, reset the resource bundle |
| * if the localization has changed and fire a notification that the model has changed</li> |
| * </ul> |
| * |
| * @param file the manifest file that was modified |
| * @param delta the resource delta |
| */ |
| private void handleBundleManifestDelta(IFile file, IResourceDelta delta) { |
| int kind = delta.getKind(); |
| IProject project = file.getProject(); |
| IPluginModelBase model = getModel(project); |
| if (kind == IResourceDelta.REMOVED && model != null) { |
| removeModel(project); |
| // switch to legacy plugin structure, if applicable |
| createModel(project, true); |
| } else if (kind == IResourceDelta.ADDED || model == null) { |
| createModel(project, true); |
| } else if (kind == IResourceDelta.CHANGED && (IResourceDelta.CONTENT & delta.getFlags()) != 0) { |
| if (model instanceof IBundlePluginModelBase) { |
| // check to see if localization changed (bug 146912) |
| String oldLocalization = ((IBundlePluginModelBase) model).getBundleLocalization(); |
| IBundleModel bmodel = ((IBundlePluginModelBase) model).getBundleModel(); |
| boolean wasFragment = bmodel.isFragmentModel(); |
| Map<String, IManifestHeader> oldHeaders = bmodel.getBundle().getManifestHeaders(); |
| |
| loadModel(bmodel, true); |
| String newLocalization = ((IBundlePluginModelBase) model).getBundleLocalization(); |
| |
| // Fragment-Host header was added or removed |
| if (wasFragment != bmodel.isFragmentModel()) { |
| removeModel(project); |
| createModel(project, true); |
| } else { |
| if (model instanceof AbstractNLModel && (oldLocalization != null && (newLocalization == null || !oldLocalization.equals(newLocalization))) || (newLocalization != null && (oldLocalization == null || !newLocalization.equals(oldLocalization)))) { |
| ((AbstractNLModel) model).resetNLResourceHelper(); |
| } |
| |
| Map<String, IManifestHeader> newHeaders = bmodel.getBundle().getManifestHeaders(); |
| if (hasModelChanged(oldHeaders, newHeaders)) { |
| addChange(model, IModelProviderEvent.MODELS_CHANGED); |
| } |
| } |
| } |
| } |
| } |
| |
| private boolean hasModelChanged(Map<String, IManifestHeader> oldHeaders, Map<String, IManifestHeader> newHeaders) { |
| oldHeaders.keySet().retainAll(RELEVANT_HEADERS); |
| newHeaders.keySet().retainAll(RELEVANT_HEADERS); |
| |
| if (oldHeaders.size() != newHeaders.size()) { |
| return true; |
| } |
| |
| for (Map.Entry<String, IManifestHeader> entry : oldHeaders.entrySet()) { |
| String key = entry.getKey(); |
| IManifestHeader oldValue = entry.getValue(); |
| IManifestHeader newValue = newHeaders.get(key); |
| |
| if (newValue == null) { |
| return true; |
| } |
| |
| if (!oldValue.getValue().equals(newValue.getValue())) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Removes the model associated with the given project from the table, |
| * if the given project is a plug-in project |
| */ |
| @Override |
| protected IPluginModelBase removeModel(IProject project) { |
| IPluginModelBase model = getModelsMap().remove(project); |
| addChange(model, IModelProviderEvent.MODELS_REMOVED); |
| |
| if (model != null && PDEProject.getOptionsFile(project).exists()) { |
| PDECore.getDefault().getTracingOptionsManager().reset(); |
| } |
| if (model != null) { |
| // PluginModelManager will remove IPluginModelBase form ModelEntry before triggering IModelChangedEvent |
| // Therefore, if we want to track a removed model we need to create an entry for it in the ExtensionDeltaEvent |
| // String id = ((IPluginModelBase)model).getPluginBase().getId(); |
| // ModelEntry entry = PluginRegistry.findEntry(id); |
| // if (entry.getWorkspaceModels().length + entry.getExternalModels().length < 2) |
| addExtensionChange(model, IModelProviderEvent.MODELS_REMOVED); |
| } |
| return model; |
| } |
| |
| /** |
| * Returns a plug-in model associated with the given project, or <code>null</code> |
| * if the project is not a plug-in project or the manifest file is missing vital data |
| * such as a symbolic name or version |
| * |
| * @param project the given project |
| * |
| * @return a plug-in model associated with the given project or <code>null</code> |
| * if no such valid model exists |
| */ |
| @Override |
| protected IPluginModelBase getModel(IProject project) { |
| return super.getModel(project); |
| } |
| |
| /** |
| * Returns a list of all workspace plug-in models |
| * |
| * @return an array of workspace plug-in models |
| */ |
| protected IPluginModelBase[] getPluginModels() { |
| initialize(); |
| return getModelsMap().values().toArray(new IPluginModelBase[getModelsMap().size()]); |
| } |
| |
| /** |
| * Adds listeners to the workspace and to the java model |
| * to be notified of PRE_CLOSE events and POST_CHANGE events. |
| */ |
| @Override |
| protected void addListeners() { |
| super.addListeners(); |
| // Overwrite resource-change event-mask set in the super implementation: |
| PDECore.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.PRE_CLOSE); |
| // PDE must process the POST_CHANGE events before the Java model |
| // for the PDE container classpath update to proceed smoothly |
| JavaCore.addPreProcessingResourceChangedListener(this, IResourceChangeEvent.POST_CHANGE); |
| } |
| |
| /** |
| * Removes listeners that the model manager attached on others, |
| * as well as listeners attached on the model manager |
| */ |
| @Override |
| protected void removeListeners() { |
| JavaCore.removePreProcessingResourceChangedListener(this); |
| if (!fExtensionListeners.isEmpty()) { |
| fExtensionListeners.clear(); |
| } |
| super.removeListeners(); |
| } |
| |
| /** |
| * Returns true if the folder being visited is of interest to PDE. |
| * In this case, PDE is only interested in META-INF folders at the root of a plug-in project |
| * We are also interested in schema folders |
| * |
| * @return <code>true</code> if the folder (and its children) is of interest to PDE; |
| * <code>false</code> otherwise. |
| * |
| */ |
| @Override |
| protected boolean isInterestingFolder(IFolder folder) { |
| IContainer root = PDEProject.getBundleRoot(folder.getProject()); |
| if (folder.getProjectRelativePath().isPrefixOf(root.getProjectRelativePath())) { |
| return true; |
| } |
| String folderName = folder.getName(); |
| if (("META-INF".equals(folderName) || "OSGI-INF".equals(folderName) || "schema".equals(folderName)) && folder.getParent().equals(root)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| return true; |
| } |
| if ("OSGI-INF/l10n".equals(folder.getProjectRelativePath().toString())) { //$NON-NLS-1$ |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Return URLs to projects in the workspace that have a manifest file (MANIFEST.MF |
| * or plugin.xml) |
| * |
| * @return an array of URLs to workspace plug-ins |
| */ |
| protected URL[] getPluginPaths() { |
| ArrayList<URL> list = new ArrayList<>(); |
| IProject[] projects = PDECore.getWorkspace().getRoot().getProjects(); |
| for (final IProject project : projects) { |
| if (isPluginProject(project)) { |
| try { |
| final IPath path = project.getLocation(); |
| if (path != null) { |
| list.add(path.toFile().toURL()); |
| } |
| } catch (MalformedURLException e) { |
| } |
| } |
| } |
| return list.toArray(new URL[list.size()]); |
| } |
| |
| void addExtensionDeltaListener(IExtensionDeltaListener listener) { |
| if (!fExtensionListeners.contains(listener)) { |
| fExtensionListeners.add(listener); |
| } |
| } |
| |
| void removeExtensionDeltaListener(IExtensionDeltaListener listener) { |
| fExtensionListeners.remove(listener); |
| } |
| |
| public void fireExtensionDeltaEvent(IExtensionDeltaEvent event) { |
| for (ListIterator<IExtensionDeltaListener> li = fExtensionListeners.listIterator(); li.hasNext();) { |
| li.next().extensionsChanged(event); |
| } |
| } |
| |
| @Override |
| protected void processModelChanges() { |
| // process model changes first so model manager is accurate when we process extension events - bug 209155 |
| super.processModelChanges(); |
| processModelChanges("org.eclipse.pde.internal.core.IExtensionDeltaEvent", fChangedExtensions); //$NON-NLS-1$ |
| fChangedExtensions = null; |
| } |
| |
| @Override |
| protected void createAndFireEvent(String eventId, int type, Collection<IModel> added, Collection<IModel> removed, Collection<IModel> changed) { |
| if (eventId.equals("org.eclipse.pde.internal.core.IExtensionDeltaEvent")) { //$NON-NLS-1$ |
| IExtensionDeltaEvent event = new ExtensionDeltaEvent(type, added.toArray(new IPluginModelBase[added.size()]), removed.toArray(new IPluginModelBase[removed.size()]), changed.toArray(new IPluginModelBase[changed.size()])); |
| fireExtensionDeltaEvent(event); |
| } else { |
| super.createAndFireEvent(eventId, type, added, removed, changed); |
| } |
| } |
| |
| protected void addExtensionChange(IPluginModelBase plugin, int type) { |
| if (fChangedExtensions == null) { |
| fChangedExtensions = new ArrayList<>(); |
| } |
| ModelChange change = new ModelChange(plugin, type); |
| fChangedExtensions.add(change); |
| } |
| } |