001: /*******************************************************************************
002: * Copyright (c) 2006, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.pde.internal.core;
011:
012: import java.io.File;
013: import java.io.FileOutputStream;
014: import java.io.IOException;
015: import java.io.OutputStream;
016: import java.io.PrintWriter;
017: import java.net.MalformedURLException;
018: import java.net.URL;
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.Collections;
022: import java.util.HashMap;
023: import java.util.ListIterator;
024:
025: import org.eclipse.core.resources.IFile;
026: import org.eclipse.core.resources.IFolder;
027: import org.eclipse.core.resources.IProject;
028: import org.eclipse.core.resources.IResourceChangeEvent;
029: import org.eclipse.core.resources.IResourceDelta;
030: import org.eclipse.core.resources.IWorkspace;
031: import org.eclipse.core.runtime.CoreException;
032: import org.eclipse.core.runtime.IPath;
033: import org.eclipse.core.runtime.Path;
034: import org.eclipse.jdt.core.JavaCore;
035: import org.eclipse.pde.core.IModelProviderEvent;
036: import org.eclipse.pde.core.plugin.IPluginModelBase;
037: import org.eclipse.pde.core.plugin.ISharedExtensionsModel;
038: import org.eclipse.pde.internal.core.builders.SchemaTransformer;
039: import org.eclipse.pde.internal.core.bundle.BundleFragmentModel;
040: import org.eclipse.pde.internal.core.bundle.BundlePluginModel;
041: import org.eclipse.pde.internal.core.bundle.WorkspaceBundleModel;
042: import org.eclipse.pde.internal.core.ibundle.IBundleModel;
043: import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
044: import org.eclipse.pde.internal.core.ischema.ISchema;
045: import org.eclipse.pde.internal.core.ischema.ISchemaDescriptor;
046: import org.eclipse.pde.internal.core.plugin.WorkspaceExtensionsModel;
047: import org.eclipse.pde.internal.core.plugin.WorkspaceFragmentModel;
048: import org.eclipse.pde.internal.core.plugin.WorkspacePluginModel;
049: import org.eclipse.pde.internal.core.schema.SchemaDescriptor;
050:
051: public class WorkspacePluginModelManager extends WorkspaceModelManager {
052:
053: private ArrayList fExtensionListeners = new ArrayList();
054: private ArrayList fChangedExtensions = null;
055:
056: /**
057: * The workspace plug-in model manager is only interested
058: * in changes to plug-in projects.
059: */
060: protected boolean isInterestingProject(IProject project) {
061: return isPluginProject(project);
062: }
063:
064: /**
065: * Creates a plug-in model based on the project structure.
066: * <p>
067: * A bundle model is created if the project has a MANIFEST.MF file and optionally
068: * a plugin.xml/fragment.xml file.
069: * </p>
070: * <p>
071: * An old-style plugin model is created if the project only has a plugin.xml/fragment.xml
072: * file.
073: * </p>
074: */
075: protected void createModel(IProject project, boolean notify) {
076: IPluginModelBase model = null;
077: if (project.exists(ICoreConstants.MANIFEST_PATH)) {
078: WorkspaceBundleModel bmodel = new WorkspaceBundleModel(
079: project.getFile(ICoreConstants.MANIFEST_PATH));
080: loadModel(bmodel, false);
081: if (bmodel.isFragmentModel())
082: model = new BundleFragmentModel();
083: else
084: model = new BundlePluginModel();
085: model.setEnabled(true);
086: ((IBundlePluginModelBase) model).setBundleModel(bmodel);
087:
088: IFile efile = project
089: .getFile(bmodel.isFragmentModel() ? ICoreConstants.FRAGMENT_PATH
090: : ICoreConstants.PLUGIN_PATH);
091: if (efile.exists()) {
092: WorkspaceExtensionsModel extModel = new WorkspaceExtensionsModel(
093: efile);
094: extModel.setEditable(false);
095: loadModel(extModel, false);
096: ((IBundlePluginModelBase) model)
097: .setExtensionsModel(extModel);
098: extModel.setBundleModel((IBundlePluginModelBase) model);
099: }
100:
101: } else if (project.exists(ICoreConstants.PLUGIN_PATH)) {
102: model = new WorkspacePluginModel(project
103: .getFile(ICoreConstants.PLUGIN_PATH), true);
104: loadModel(model, false);
105: } else if (project.exists(ICoreConstants.FRAGMENT_PATH)) {
106: model = new WorkspaceFragmentModel(project
107: .getFile(ICoreConstants.FRAGMENT_PATH), true);
108: loadModel(model, false);
109: }
110:
111: if (project.getFile(".options").exists()) //$NON-NLS-1$
112: PDECore.getDefault().getTracingOptionsManager().reset();
113:
114: if (model != null) {
115: if (fModels == null)
116: fModels = new HashMap();
117: fModels.put(project, model);
118: if (notify)
119: addChange(model, IModelProviderEvent.MODELS_ADDED);
120: }
121: }
122:
123: /**
124: * Reacts to changes in files of interest to PDE
125: */
126: protected void handleFileDelta(IResourceDelta delta) {
127: IFile file = (IFile) delta.getResource();
128: String filename = file.getName();
129: if (filename.equals(".options")) { //$NON-NLS-1$
130: PDECore.getDefault().getTracingOptionsManager().reset();
131: } else if (filename.endsWith(".properties")) { //$NON-NLS-1$
132: // change in build.properties should trigger a Classpath Update
133: // we therefore fire a notification
134: //TODO this is inefficient. we could do better.
135: if (filename.equals("build.properties")) { //$NON-NLS-1$
136: Object model = getModel(file.getProject());
137: if (model != null)
138: addChange(model, IModelProviderEvent.MODELS_CHANGED);
139: } else {
140: // reset bundle resource if localization file has changed.
141: IPluginModelBase model = getPluginModel(file
142: .getProject());
143: String localization = null;
144: if (model instanceof IBundlePluginModelBase) {
145: localization = ((IBundlePluginModelBase) model)
146: .getBundleLocalization();
147: } else if (model != null) {
148: localization = "plugin"; //$NON-NLS-1$
149: }
150: if (localization != null
151: && filename.startsWith(localization)) {
152: ((AbstractNLModel) model).resetNLResourceHelper();
153: }
154: }
155: } else if (filename.endsWith(".exsd")) { //$NON-NLS-1$
156: handleEclipseSchemaDelta(file, delta);
157: } else {
158: IPath path = file.getProjectRelativePath();
159: if (path.equals(ICoreConstants.PLUGIN_PATH)
160: || path.equals(ICoreConstants.FRAGMENT_PATH)) {
161: handleExtensionFileDelta(file, delta);
162: } else if (path.equals(ICoreConstants.MANIFEST_PATH)) {
163: handleBundleManifestDelta(file, delta);
164: }
165: }
166: }
167:
168: /**
169: * @param file
170: * @param delta
171: */
172: private void handleEclipseSchemaDelta(IFile schemaFile,
173: IResourceDelta delta) {
174: // Get the kind of resource delta
175: int kind = delta.getKind();
176: // We are only interested in schema files whose contents have changed
177: if (kind != IResourceDelta.CHANGED) {
178: return;
179: } else if ((IResourceDelta.CONTENT & delta.getFlags()) == 0) {
180: return;
181: }
182: // Get the schema preview file session property
183: Object property = null;
184: try {
185: property = schemaFile
186: .getSessionProperty(PDECore.SCHEMA_PREVIEW_FILE);
187: } catch (CoreException e) {
188: // Ignore
189: return;
190: }
191: // Check if the schema file has an associated HTML schema preview file
192: // (That is, whether a show description action has been executed before)
193: // Property set in
194: // org.eclipse.pde.internal.ui.search.ShowDescriptionAction.linkPreviewFileToSchemaFile()
195: if (property == null) {
196: return;
197: } else if ((property instanceof File) == false) {
198: return;
199: }
200: File schemaPreviewFile = (File) property;
201: // Ensure the file exists and is writable
202: if (schemaPreviewFile.exists() == false) {
203: return;
204: } else if (schemaPreviewFile.isFile() == false) {
205: return;
206: } else if (schemaPreviewFile.canWrite() == false) {
207: return;
208: }
209: // Get the schema model object
210: ISchemaDescriptor descriptor = new SchemaDescriptor(schemaFile,
211: false);
212: ISchema schema = descriptor.getSchema(false);
213:
214: try {
215: // Re-generate the schema preview file contents in order to reflect
216: // the changes in the schema
217: recreateSchemaPreviewFileContents(schemaPreviewFile, schema);
218: } catch (IOException e) {
219: // Ignore
220: }
221: }
222:
223: /**
224: * @param schemaPreviewFile
225: * @param schema
226: * @throws IOException
227: */
228: private void recreateSchemaPreviewFileContents(
229: File schemaPreviewFile, ISchema schema) throws IOException {
230: SchemaTransformer transformer = new SchemaTransformer();
231: OutputStream os = new FileOutputStream(schemaPreviewFile);
232: PrintWriter printWriter = new PrintWriter(os, true);
233: transformer.transform(schema, printWriter);
234: os.flush();
235: os.close();
236: }
237:
238: /**
239: * Reacts to changes in the plugin.xml or fragment.xml file.
240: * <ul>
241: * <li>If the file has been deleted and the project has a MANIFEST.MF file,
242: * then this deletion only affects extensions and extension points.</li>
243: * <li>If the file has been deleted and the project does not have a MANIFEST.MF file,
244: * then it's an old-style plug-in and the entire model must be removed from the table.</li>
245: * <li>If the file has been added and the project already has a MANIFEST.MF, then
246: * this file only contributes extensions and extensions. No need to send a notification
247: * to trigger update classpath of dependent plug-ins</li>
248: * <li>If the file has been added and the project does not have a MANIFEST.MF, then
249: * an old-style plug-in has been created.</li>
250: * <li>If the file has been modified and the project already has a MANIFEST.MF,
251: * then reload the extensions model but do not send out notifications</li>
252: * </li>If the file has been modified and the project has no MANIFEST.MF, then
253: * it's an old-style plug-in, reload and send out notifications to trigger a classpath update
254: * for dependent plug-ins</li>
255: * </ul>
256: * @param file the manifest file
257: * @param delta the resource delta
258: */
259: private void handleExtensionFileDelta(IFile file,
260: IResourceDelta delta) {
261: int kind = delta.getKind();
262: IPluginModelBase model = (IPluginModelBase) getModel(file
263: .getProject());
264: if (kind == IResourceDelta.REMOVED) {
265: if (model instanceof IBundlePluginModelBase) {
266: ((IBundlePluginModelBase) model)
267: .setExtensionsModel(null);
268: addExtensionChange(model,
269: IModelProviderEvent.MODELS_REMOVED);
270: } else {
271: removeModel(file.getProject());
272: }
273: } else if (kind == IResourceDelta.ADDED) {
274: if (model instanceof IBundlePluginModelBase) {
275: WorkspaceExtensionsModel extensions = new WorkspaceExtensionsModel(
276: file);
277: extensions.setEditable(false);
278: ((IBundlePluginModelBase) model)
279: .setExtensionsModel(extensions);
280: extensions
281: .setBundleModel((IBundlePluginModelBase) model);
282: loadModel(extensions, false);
283: addExtensionChange(model,
284: IModelProviderEvent.MODELS_REMOVED);
285: } else {
286: createModel(file.getProject(), true);
287: }
288: } else if (kind == IResourceDelta.CHANGED
289: && (IResourceDelta.CONTENT & delta.getFlags()) != 0) {
290: if (model instanceof IBundlePluginModelBase) {
291: ISharedExtensionsModel extensions = ((IBundlePluginModelBase) model)
292: .getExtensionsModel();
293: boolean reload = extensions != null;
294: if (extensions == null) {
295: extensions = new WorkspaceExtensionsModel(file);
296: ((WorkspaceExtensionsModel) extensions)
297: .setEditable(false);
298: ((IBundlePluginModelBase) model)
299: .setExtensionsModel(extensions);
300: ((WorkspaceExtensionsModel) extensions)
301: .setBundleModel((IBundlePluginModelBase) model);
302: }
303: loadModel(extensions, reload);
304: } else if (model != null) {
305: loadModel(model, true);
306: addChange(model, IModelProviderEvent.MODELS_CHANGED);
307: }
308: addExtensionChange(model,
309: IModelProviderEvent.MODELS_CHANGED);
310: }
311: }
312:
313: /**
314: * Reacts to changes in the MANIFEST.MF file.
315: * <ul>
316: * <li>If the file has been deleted, switch to the old-style plug-in if a plugin.xml file exists</li>
317: * <li>If the file has been added, create a new bundle model</li>
318: * <li>If the file has been modified, reload the model, reset the resource bundle
319: * if the localization has changed and fire a notification that the model has changed</li>
320: * </ul>
321: *
322: * @param file the manifest file that was modified
323: * @param delta the resource delta
324: */
325: private void handleBundleManifestDelta(IFile file,
326: IResourceDelta delta) {
327: int kind = delta.getKind();
328: IProject project = file.getProject();
329: Object model = getModel(project);
330: if (kind == IResourceDelta.REMOVED && model != null) {
331: removeModel(project);
332: // switch to legacy plugin structure, if applicable
333: createModel(project, true);
334: } else if (kind == IResourceDelta.ADDED || model == null) {
335: createModel(project, true);
336: } else if (kind == IResourceDelta.CHANGED
337: && (IResourceDelta.CONTENT & delta.getFlags()) != 0) {
338: if (model instanceof IBundlePluginModelBase) {
339: // check to see if localization changed (bug 146912)
340: String oldLocalization = ((IBundlePluginModelBase) model)
341: .getBundleLocalization();
342: IBundleModel bmodel = ((IBundlePluginModelBase) model)
343: .getBundleModel();
344: boolean wasFragment = bmodel.isFragmentModel();
345: loadModel(bmodel, true);
346: String newLocalization = ((IBundlePluginModelBase) model)
347: .getBundleLocalization();
348:
349: // Fragment-Host header was added or removed
350: if (wasFragment != bmodel.isFragmentModel()) {
351: removeModel(project);
352: createModel(project, true);
353: } else {
354: if (model instanceof AbstractNLModel
355: && (oldLocalization != null && (newLocalization == null || !oldLocalization
356: .equals(newLocalization)))
357: || (newLocalization != null && (oldLocalization == null || !newLocalization
358: .equals(oldLocalization))))
359: ((AbstractNLModel) model)
360: .resetNLResourceHelper();
361: addChange(model, IModelProviderEvent.MODELS_CHANGED);
362: }
363: }
364: }
365: }
366:
367: /**
368: * Removes the model associated with the given project from the table,
369: * if the given project is a plug-in project
370: */
371: protected Object removeModel(IProject project) {
372: Object model = super .removeModel(project);
373: if (model != null && project.exists(new Path(".options"))) //$NON-NLS-1$
374: PDECore.getDefault().getTracingOptionsManager().reset();
375: if (model instanceof IPluginModelBase) {
376: // PluginModelManager will remove IPluginModelBase form ModelEntry before triggering IModelChangedEvent
377: // Therefore, if we want to track a removed model we need to create an entry for it in the ExtensionDeltaEvent
378: // String id = ((IPluginModelBase)model).getPluginBase().getId();
379: // ModelEntry entry = PluginRegistry.findEntry(id);
380: // if (entry.getWorkspaceModels().length + entry.getExternalModels().length < 2)
381: addExtensionChange((IPluginModelBase) model,
382: IModelProviderEvent.MODELS_REMOVED);
383: }
384: return model;
385: }
386:
387: /**
388: * Returns a plug-in model associated with the given project, or <code>null</code>
389: * if the project is not a plug-in project or the manifest file is missing vital data
390: * such as a symbolic name or version
391: *
392: * @param project the given project
393: *
394: * @return a plug-in model associated with the given project or <code>null</code>
395: * if no such valid model exists
396: */
397: protected IPluginModelBase getPluginModel(IProject project) {
398: return (IPluginModelBase) getModel(project);
399: }
400:
401: /**
402: * Returns a list of all workspace plug-in models
403: *
404: * @return an array of workspace plug-in models
405: */
406: protected IPluginModelBase[] getPluginModels() {
407: initialize();
408: return (IPluginModelBase[]) fModels.values().toArray(
409: new IPluginModelBase[fModels.size()]);
410: }
411:
412: /**
413: * Adds listeners to the workspace and to the java model
414: * to be notified of PRE_CLOSE events and POST_CHANGE events.
415: */
416: protected void addListeners() {
417: IWorkspace workspace = PDECore.getWorkspace();
418: workspace.addResourceChangeListener(this ,
419: IResourceChangeEvent.PRE_CLOSE);
420: // PDE must process the POST_CHANGE events before the Java model
421: // for the PDE container classpath update to proceed smoothly
422: JavaCore.addPreProcessingResourceChangedListener(this ,
423: IResourceChangeEvent.POST_CHANGE);
424: }
425:
426: /**
427: * Removes listeners that the model manager attached on others,
428: * as well as listeners attached on the model manager
429: */
430: protected void removeListeners() {
431: PDECore.getWorkspace().removeResourceChangeListener(this );
432: JavaCore.removePreProcessingResourceChangedListener(this );
433: if (fExtensionListeners.size() > 0)
434: fExtensionListeners.clear();
435: super .removeListeners();
436: }
437:
438: /**
439: * Returns true if the folder being visited is of interest to PDE.
440: * In this case, PDE is only interested in META-INF folders at the root of a plug-in project
441: * We are also interested in schema folders
442: *
443: * @return <code>true</code> if the folder (and its children) is of interest to PDE;
444: * <code>false</code> otherwise.
445: *
446: */
447: protected boolean isInterestingFolder(IFolder folder) {
448: if (folder.getName().equals("META-INF") && folder.getParent() instanceof IProject) { //$NON-NLS-1$
449: return true;
450: }
451:
452: if (folder.getName().equals("schema") && folder.getParent() instanceof IProject) { //$NON-NLS-1$
453: return true;
454: }
455:
456: return false;
457: }
458:
459: /**
460: * This method is called when workspace models are read and initialized
461: * from the cache. No need to read the workspace plug-ins from scratch.
462: *
463: * @param models the workspace plug-in models
464: */
465: protected void initializeModels(IPluginModelBase[] models) {
466: fModels = Collections.synchronizedMap(new HashMap());
467: for (int i = 0; i < models.length; i++) {
468: IProject project = models[i].getUnderlyingResource()
469: .getProject();
470: fModels.put(project, models[i]);
471: }
472: addListeners();
473: }
474:
475: /**
476: * Return URLs to projects in the workspace that have a manifest file (MANIFEST.MF
477: * or plugin.xml)
478: *
479: * @return an array of URLs to workspace plug-ins
480: */
481: protected URL[] getPluginPaths() {
482: ArrayList list = new ArrayList();
483: IProject[] projects = PDECore.getWorkspace().getRoot()
484: .getProjects();
485: for (int i = 0; i < projects.length; i++) {
486: if (isPluginProject(projects[i])) {
487: try {
488: IPath path = projects[i].getLocation();
489: if (path != null) {
490: list.add(path.toFile().toURL());
491: }
492: } catch (MalformedURLException e) {
493: }
494: }
495: }
496: return (URL[]) list.toArray(new URL[list.size()]);
497: }
498:
499: void addExtensionDeltaListener(IExtensionDeltaListener listener) {
500: if (!fExtensionListeners.contains(listener))
501: fExtensionListeners.add(listener);
502: }
503:
504: void removeExtensionDeltaListener(IExtensionDeltaListener listener) {
505: fExtensionListeners.remove(listener);
506: }
507:
508: public void fireExtensionDeltaEvent(IExtensionDeltaEvent event) {
509: for (ListIterator li = fExtensionListeners.listIterator(); li
510: .hasNext();) {
511: ((IExtensionDeltaListener) li.next())
512: .extensionsChanged(event);
513: }
514: }
515:
516: protected void processModelChanges() {
517: processModelChanges(
518: "org.eclipse.pde.internal.core.IExtensionDeltaEvent", fChangedExtensions); //$NON-NLS-1$
519: fChangedExtensions = null;
520: super .processModelChanges();
521: }
522:
523: protected void createAndFireEvent(String eventId, int type,
524: Collection added, Collection removed, Collection changed) {
525: if (eventId
526: .equals("org.eclipse.pde.internal.core.IExtensionDeltaEvent")) { //$NON-NLS-1$
527: IExtensionDeltaEvent event = new ExtensionDeltaEvent(
528: type,
529: (IPluginModelBase[]) added
530: .toArray(new IPluginModelBase[added.size()]),
531: (IPluginModelBase[]) removed
532: .toArray(new IPluginModelBase[removed
533: .size()]),
534: (IPluginModelBase[]) changed
535: .toArray(new IPluginModelBase[changed
536: .size()]));
537: fireExtensionDeltaEvent(event);
538: } else
539: super .createAndFireEvent(eventId, type, added, removed,
540: changed);
541: }
542:
543: protected void addExtensionChange(IPluginModelBase plugin, int type) {
544: if (fChangedExtensions == null)
545: fChangedExtensions = new ArrayList();
546: ModelChange change = new ModelChange(plugin, type);
547: fChangedExtensions.add(change);
548: }
549: }
|