/******************************************************************************* * Copyright (c) 2007, 2011 Intel Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Intel Corporation - Initial API and implementation * Anton Leherbauer (Wind River Systems) * James Blackburn (Broadcom Corp.) * IBM Corporation *******************************************************************************/ package org.eclipse.cdt.internal.core; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import org.eclipse.cdt.core.AbstractCExtension; import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.CDescriptorEvent; import org.eclipse.cdt.core.ICDescriptor; import org.eclipse.cdt.core.ICDescriptorManager; import org.eclipse.cdt.core.ICExtension; import org.eclipse.cdt.core.ICExtensionReference; import org.eclipse.cdt.core.ICOwnerInfo; import org.eclipse.cdt.core.settings.model.ICConfigExtensionReference; import org.eclipse.cdt.core.settings.model.ICConfigurationDescription; import org.eclipse.cdt.core.settings.model.ICProjectDescription; import org.eclipse.cdt.core.settings.model.ICStorageElement; import org.eclipse.cdt.core.settings.model.util.CDataUtil; import org.eclipse.cdt.core.settings.model.util.CExtensionUtil; import org.eclipse.cdt.internal.core.settings.model.CConfigurationDescriptionCache; import org.eclipse.cdt.internal.core.settings.model.CConfigurationSpecSettings; import org.eclipse.cdt.internal.core.settings.model.CProjectDescriptionManager; import org.eclipse.cdt.internal.core.settings.model.ExceptionFactory; import org.eclipse.cdt.internal.core.settings.model.IInternalCCfgInfo; import org.eclipse.cdt.internal.core.settings.model.SynchronizedStorageElement; import org.eclipse.cdt.internal.core.settings.model.xml.XmlStorage; import org.eclipse.cdt.internal.core.settings.model.xml.XmlStorageElement; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ILock; import org.eclipse.core.runtime.jobs.Job; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.ibm.icu.text.MessageFormat; /** * Concrete ICDescriptor for a Project. * * There is only one of these per project. Settings are serialized as storage elements * as children of the root of the project description. Methods which change or access data * on the descriptor use the Eclipse ILock 'fLock' on the given descriptor instance. * * Structural changes made to extension elements are persisted immediately to * the project description. * * Changes made to child storage elements are serialized to the project description * with saveProjectData(...) and the serializingJob. * * Users should consider using {@link ICDescriptorManager#runDescriptorOperation} for threadsafe * access to the project's configuration. However failing this does provide some basic * concurrency on {@link #getProjectStorageElement(String)} by wrapping the returned * ICStorageElement in an {@link SynchronizedStorageElement}. Note that this is best * effort, so concurrent structural changes to the tree (such as one thread removing * an element from a tree while another is writing to it) may result in inconsistent data * stored. * */ final public class CConfigBasedDescriptor implements ICDescriptor { private static final String CEXTENSION_NAME = "cextension"; //$NON-NLS-1$ /** The current default setting configuration description * Equivalent to {@link ICProjectDescription#getDefaultSettingConfiguration()}*/ private ICConfigurationDescription fCfgDes; private COwner fOwner; /** Map: storageModule ID -> ICStorageElement <br/> * CDescriptor's map of so far uncommited storage elements. */ private final Map<String, SynchronizedStorageElement> fStorageDataElMap = new HashMap<String, SynchronizedStorageElement>(); private volatile boolean fIsDirty; /** Current CDescriptor Event which tracks changes between operationStart & operationStop */ private CDescriptorEvent fOpEvent; /** Flag indicating whether an operation has started */ private volatile boolean fIsOpStarted; /** This descriptor's lock */ final ILock fLock = Job.getJobManager().newLock(); /** * The Job the actually does the data applying (by getting and setting the current project description) * saveProjectData never does the saving itself, rather it schedules this job to run. * During the setCProjectDescriptionOperation the changes in this ICDescriptor are synchronized into the * project description being persisted. */ class SerializingJob extends Job { public SerializingJob(String name) { super (name); setSystem(true); // This rule must contain that in SetCProjectDescriptionOperation // (Resource scheduling rules are always obtained before data structure locks to prevent deadlocks.) setRule(ResourcesPlugin.getWorkspace().getRoot()); } @Override protected IStatus run(IProgressMonitor monitor) { try { fLock.acquire(); // No point scheduling the run if the project is closed... if (!getProject().isAccessible()) return Status.CANCEL_STATUS; serialize(); } catch (CoreException e) { CCorePlugin.log(e); } finally { fLock.release(); } return Status.OK_STATUS; } public void serialize() throws CoreException { if (!getProject().isAccessible()) throw ExceptionFactory.createCoreException(MessageFormat.format(CCorePlugin.getResourceString("ProjectDescription.ProjectNotAccessible"), new Object[] {getProject().getName()})); //$NON-NLS-1$ if(fIsDirty) { ICProjectDescription des = fCfgDes.getProjectDescription(); if(des.isCdtProjectCreating()) des.setCdtProjectCreated(); CProjectDescriptionManager.getInstance().setProjectDescription(getProject(), des); fIsDirty = false; } } } SerializingJob serializingJob = new SerializingJob("CConfigBasedDescriptor Serializing Job"); //$NON-NLS-1$ (system) /** * Concrete implementation of ICExtensionReference based on ICConfigExtensionReference elements. * In the old world ICExtensions had no notion of which configuration they belong to. * As a result all state that would have be persisted at the ICExtension level is saved to all * the configurations in the project * * This is a lightweight proxy onto ICConfigExtensionReference and doesn't hold any state * itself (though alters the isDirty state and descriptor event of the containing Descriptor). */ final class CConfigBaseDescriptorExtensionReference implements ICExtensionReference { /** The ICConfigExtensionReference this is based on -- the identifying feature of this ICExtensionReference */ private final ICConfigExtensionReference fCfgExtRef; CConfigBaseDescriptorExtensionReference(ICConfigExtensionReference cfgRef){ fCfgExtRef = cfgRef; } public ICExtension createExtension() throws CoreException { AbstractCExtension cExtension = null; IConfigurationElement el = CExtensionUtil.getFirstConfigurationElement(fCfgExtRef, CEXTENSION_NAME, false); cExtension = (AbstractCExtension)el.createExecutableExtension("run"); //$NON-NLS-1$ cExtension.setExtensionReference(fCfgExtRef); cExtension.setProject(getProject()); return cExtension; } public ICDescriptor getCDescriptor() { return CConfigBasedDescriptor.this; } public String getExtension() { return fCfgExtRef.getExtensionPoint(); } public String getExtensionData(String key) { return fCfgExtRef.getExtensionData(key); } public IConfigurationElement[] getExtensionElements() throws CoreException { IConfigurationElement el = CExtensionUtil.getFirstConfigurationElement(fCfgExtRef, CEXTENSION_NAME, false); if(el != null) return el.getChildren(); return new IConfigurationElement[0]; } public String getID() { return fCfgExtRef.getID(); } public void setExtensionData(String key, String value) throws CoreException { if(!CDataUtil.objectsEqual(fCfgExtRef.getExtensionData(key), value)){ fIsDirty = true; fCfgExtRef.setExtensionData(key, value); checkApply(); if(isOperationStarted()) setOpEvent(new CDescriptorEvent(CConfigBasedDescriptor.this, CDescriptorEvent.CDTPROJECT_CHANGED, 0)); } } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof CConfigBaseDescriptorExtensionReference) return fCfgExtRef.equals(((CConfigBaseDescriptorExtensionReference)obj).fCfgExtRef); return fCfgExtRef.equals(obj); } @Override public int hashCode() { return fCfgExtRef.hashCode(); } } public CConfigBasedDescriptor(ICConfigurationDescription des) throws CoreException{ this(des, true); } public CConfigBasedDescriptor(ICConfigurationDescription des, boolean write) throws CoreException{ updateConfiguration(des, write); } /** * Persist the current project description (to persist changes to the ICExtensions) * @param force * @throws CoreException */ void apply(boolean force) throws CoreException { fIsDirty |= force; if (!fIsDirty) return; // If we're already serializing the project description, schedule a job // to perform the serialization... if (CProjectDescriptionManager.getInstance().isCurrentThreadSetProjectDescription()) { serializingJob.schedule(); return; } // Deadlock warning: path entry, for example, can do getStorageElement // in resource delta (while holding the workspace lock). As CModelOperation // runs the job as a workspace runnable, this leads to potential deadlock. // // So before applying, we ensure that we hold the project resource rule // before getting the 'lock' on the datastructures // final IProject project = getProject(); // Release the lock final int lockDepth = fLock.getDepth(); for (int i = 0; i < lockDepth ; ++i) fLock.release(); try { // This rule must contain that in SetCProjectDescriptionOperation Job.getJobManager().beginRule(ResourcesPlugin.getWorkspace().getRoot(), new NullProgressMonitor()); try { fLock.acquire(); serializingJob.serialize(); } finally { if (lockDepth == 0) // Only release the lock if it wasn't previously held on entrance to this method fLock.release(); else // Reacquire the lock to the appropriate depth for (int i = 0; i < lockDepth - 1; i++) fLock.acquire(); } } finally { Job.getJobManager().endRule(ResourcesPlugin.getWorkspace().getRoot()); } } private void checkApply() throws CoreException { apply(false); } /** * Set the dirty flag * @param dirty */ void setDirty(boolean dirty){ fIsDirty = dirty; } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#create(java.lang.String, java.lang.String) */ public ICExtensionReference create(String extensionPoint, String id) throws CoreException { try { fLock.acquire(); ICConfigExtensionReference ref = fCfgDes.create(extensionPoint, id); //write is done for all configurations to avoid "data loss" on configuration change ICProjectDescription des = fCfgDes.getProjectDescription(); ICConfigurationDescription cfgs[] = des.getConfigurations(); for (ICConfigurationDescription cfg : cfgs) { if(cfg != fCfgDes){ try { cfg.create(extensionPoint, id); } catch (CoreException e){ CCorePlugin.log(e); } } } ICExtensionReference r = new CConfigBaseDescriptorExtensionReference(ref); fIsDirty = true; checkApply(); if(isOperationStarted()) setOpEvent(new CDescriptorEvent(this, CDescriptorEvent.CDTPROJECT_CHANGED, CDescriptorEvent.EXTENSION_CHANGED)); return r; } finally { fLock.release(); } } /** * Equivalent to {@code updateConfiguration(des, true)} * @param des the new ICConfigurationDescription * @throws CoreException */ public void updateConfiguration(ICConfigurationDescription des) throws CoreException{ updateConfiguration(des, true); } /** * Update the currently default (settings) configuration * @param des * @param write * @throws CoreException */ public void updateConfiguration(ICConfigurationDescription des, boolean write) throws CoreException{ try { fLock.acquire(); if(write && des instanceof CConfigurationDescriptionCache) throw new IllegalArgumentException(); fCfgDes = des; CConfigurationSpecSettings settings = ((IInternalCCfgInfo)fCfgDes).getSpecSettings(); fOwner = settings.getCOwner(); } finally { fLock.release(); } } /** * Attempt to return an ICExtensionReference array based on the ICConfigExtensionReferences * contained in this project description (which match the extensionPointId). * * Fetches all the ICConfigExtensionReferences from the project's configurations. * * Previously this cached the current set of ICExtensionReferences, * but this cache was never used (it was always overwritten by this method). * * FIXME re-add caching (the current behaviour mirrors the previous behaviour -- just tidier) * @return an array of ICExtenionReference */ public ICExtensionReference[] get(String extensionPoint) { try { fLock.acquire(); LinkedHashSet<ICExtensionReference> extRefs = new LinkedHashSet<ICExtensionReference>(); // Add the ICConfigExtensionReferences for the current configuration description for (ICConfigExtensionReference cfgRes : fCfgDes.get(extensionPoint)) extRefs.add(new CConfigBaseDescriptorExtensionReference(cfgRes)); for (ICConfigurationDescription cfg : fCfgDes.getProjectDescription().getConfigurations()) if (!cfg.equals(fCfgDes)) for (ICConfigExtensionReference cfgRes : fCfgDes.get(extensionPoint)) extRefs.add(new CConfigBaseDescriptorExtensionReference(cfgRes)); return extRefs.toArray(new ICExtensionReference[extRefs.size()]); } finally { fLock.release(); } } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#get(java.lang.String, boolean) */ public ICExtensionReference[] get(String extensionPoint, boolean update) throws CoreException { try { fLock.acquire(); ICExtensionReference[] refs = get(extensionPoint); if(refs.length == 0 && update){ fOwner.update(getProject(), this, extensionPoint); checkApply(); refs = get(extensionPoint); } return refs; } finally { fLock.release(); } } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#getPlatform() */ public String getPlatform() { try { fLock.acquire(); return fOwner.getPlatform(); } finally { fLock.release(); } } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#getProject() */ public IProject getProject() { try { fLock.acquire(); return fCfgDes.getProjectDescription().getProject(); } finally { fLock.release(); } } /** * Note that in the current implementation of the xml based project description * it is not safe to work on the same storage element in more than one thread. * * It is likely that doing so will return a concurrent modification exception on the * returned ICStorageElement. We must allow this as this is how the existing implementation * behaves. */ public ICStorageElement getProjectStorageElement(String id) throws CoreException { try { fLock.acquire(); // Check if the storage element already exists in our local map SynchronizedStorageElement storageEl = fStorageDataElMap.get(id); if(storageEl == null){ // Check in the Proejct Description ICStorageElement el = fCfgDes.getProjectDescription().getStorage(id, false); // Fall-back to checking in the configuration (which is how it used ot be) if (el == null) el = fCfgDes.getStorage(id, true); try { el = el.createCopy(); } catch (UnsupportedOperationException e) { throw ExceptionFactory.createCoreException(e); } storageEl = SynchronizedStorageElement.synchronizedElement(el); fStorageDataElMap.put(id, storageEl); } return storageEl; } finally { fLock.release(); } } /** * Backwards compatibility method which provides an XML Element. * Currently relies on the fact that the only implementation if ICStorageElement * in the core is XmlStorageElement. */ public Element getProjectData(String id) throws CoreException { try { fLock.acquire(); // Check if the storage element already exists in our local map SynchronizedStorageElement storageEl = fStorageDataElMap.get(id); ICStorageElement el; if(storageEl == null) { el = fCfgDes.getProjectDescription().getStorage(id, false); if (el == null) el = fCfgDes.getStorage(id, true); try { el = el.createCopy(); } catch (UnsupportedOperationException e) { throw ExceptionFactory.createCoreException(e); } if (!(el instanceof XmlStorageElement)) throw ExceptionFactory.createCoreException( "Internal Error: getProjectData(...) currently only supports XmlStorageElement types.", new Exception()); //$NON-NLS-1$ // Get the underlying Xml Element final Element xmlEl = ((XmlStorageElement)el).fElement; // This proxy synchronizes the storage element's root XML Element el = new XmlStorageElement((Element)Proxy.newProxyInstance(Element.class.getClassLoader(), new Class[]{Element.class}, new InvocationHandler(){ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Method realMethod = xmlEl.getClass().getMethod(method.getName(), method.getParameterTypes()); // Now just execute the method synchronized (xmlEl) { // If requesting the parent node, then we need another proxy // so that parent.removeChildNode(...) 'does the right thing' if (method.getName().equals("getParentNode")) { //$NON-NLS-1$ final Node parent = (Node)realMethod.invoke(xmlEl, args); Node parentProxy = (Node)Proxy.newProxyInstance(Node.class.getClassLoader(), new Class[]{Node.class}, new InvocationHandler(){ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Method realMethod = parent.getClass().getMethod(method.getName(), method.getParameterTypes()); synchronized (xmlEl) { // Handle the remove child case if (method.getName().equals("removeChild")) { //$NON-NLS-1$ if (args[0] instanceof Element && ((Element)args[0]).getAttribute( XmlStorage.MODULE_ID_ATTRIBUTE).length() > 0) { ICStorageElement removed = removeProjectStorageElement(((Element)args[0]).getAttribute( XmlStorage.MODULE_ID_ATTRIBUTE)); if (removed != null) return ((XmlStorageElement)((SynchronizedStorageElement)removed).getOriginalElement()).fElement; return null; } } // else return the realMethod return realMethod.invoke(parent, args); } } }); return parentProxy; } // Otherwise just execute the method return realMethod.invoke(xmlEl, args); } } })); storageEl = SynchronizedStorageElement.synchronizedElement(el, xmlEl); fStorageDataElMap.put(id, storageEl); } else { el = storageEl.getOriginalElement(); if (!(el instanceof XmlStorageElement)) throw ExceptionFactory.createCoreException( "Internal Error: getProjectData(...) currently only supports XmlStorageElement types.", new Exception()); //$NON-NLS-1$ } return ((XmlStorageElement)el).fElement; } finally { fLock.release(); } } public ICStorageElement removeProjectStorageElement(String id) throws CoreException { try { fLock.acquire(); return fStorageDataElMap.put(id, null); } finally { fLock.release(); } } public ICOwnerInfo getProjectOwner() { try { fLock.acquire(); return fOwner; } finally { fLock.release(); } } public void remove(ICExtensionReference extension) throws CoreException { try { fLock.acquire(); ICConfigExtensionReference ref = ((CConfigBaseDescriptorExtensionReference)extension).fCfgExtRef; fCfgDes.remove(ref); // write is done for all configurations to avoid "data loss" on configuration change for (ICConfigurationDescription cfg : fCfgDes.getProjectDescription().getConfigurations()) { if(cfg != fCfgDes){ try { ICConfigExtensionReference rs[] = cfg.get(ref.getExtensionPoint()); for (ICConfigExtensionReference element : rs) { if(ref.getID().equals(element.getID())){ cfg.remove(element); break; } } } catch (CoreException e) { CCorePlugin.log(e); } } } fIsDirty = true; checkApply(); if(isOperationStarted()) setOpEvent(new CDescriptorEvent(this, CDescriptorEvent.CDTPROJECT_CHANGED, CDescriptorEvent.EXTENSION_CHANGED)); } finally { fLock.release(); } } public void remove(String extensionPoint) throws CoreException { try { fLock.acquire(); fCfgDes.remove(extensionPoint); //write is done for all configurations to avoid "data loss" on configuration change for (ICConfigurationDescription cfg : fCfgDes.getProjectDescription().getConfigurations()) { if(cfg != fCfgDes){ try { cfg.remove(extensionPoint); } catch (CoreException e) { CCorePlugin.log(e); } } } fIsDirty = true; checkApply(); if(isOperationStarted()) setOpEvent(new CDescriptorEvent(this, CDescriptorEvent.CDTPROJECT_CHANGED, CDescriptorEvent.EXTENSION_CHANGED)); } finally { fLock.release(); } } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#saveProjectData() */ public void saveProjectData() throws CoreException { try { fLock.acquire(); // Reconcile changes into the current project description if(reconcile(this, fCfgDes.getProjectDescription())) { // Dirty => Apply fIsDirty = true; apply(true); if(isOperationStarted()) setOpEvent(new CDescriptorEvent(this, CDescriptorEvent.CDTPROJECT_CHANGED, 0)); } } finally { fLock.release(); } } /* * (non-Javadoc) * @see org.eclipse.cdt.core.ICDescriptor#getConfigurationDescription() */ public ICConfigurationDescription getConfigurationDescription() { try { fLock.acquire(); return fCfgDes; } finally { fLock.release(); } } /* * Event handling routines */ void setOpEvent(CDescriptorEvent event) { try { fLock.acquire(); if(!isOperationStarted()) return; if (event.getType() == CDescriptorEvent.CDTPROJECT_ADDED) { fOpEvent = event; } else if (event.getType() == CDescriptorEvent.CDTPROJECT_REMOVED) { fOpEvent = event; } else { if (fOpEvent == null) { fOpEvent = event; } else if ( (fOpEvent.getFlags() & event.getFlags()) != event.getFlags()) { fOpEvent = new CDescriptorEvent(event.getDescriptor(), event.getType(), fOpEvent.getFlags() | event.getFlags()); } } } finally { fLock.release(); } } boolean isOperationStarted(){ return fIsOpStarted; } void operationStart(){ fIsOpStarted = true; } /** * Mark the operation as over -- return the CDescriptorEvent * @return */ CDescriptorEvent operationStop(){ try { fLock.acquire(); fIsOpStarted = false; CDescriptorEvent e = fOpEvent; fOpEvent = null; return e; } finally { fLock.release(); } } /* * * The reconcile methods below are for copying storage element changes from the current * CConfigBasedDescriptor to the passed in writable project description * */ /** * Copies the changes made to the CConfigBasedDescriptor to the ICProjectDescription * * The changes are reconciled into all the project's configurations! * @param descriptor * @param des * @return boolean indicating whether changes were made */ public static boolean reconcile(CConfigBasedDescriptor descriptor, ICProjectDescription des) throws CoreException { try { descriptor.fLock.acquire(); Map<String, SynchronizedStorageElement> map = descriptor.fStorageDataElMap; boolean reconciled = false; if(!map.isEmpty()){ for (Map.Entry<String, SynchronizedStorageElement> entry : map.entrySet()) { String id = entry.getKey(); SynchronizedStorageElement synchStor = entry.getValue(); if (synchStor != null ) { // Lock the synchronized storage element to prevent further changes synchronized (synchStor.lock()) { if(reconcile(id, synchStor.getOriginalElement(), des)) reconciled = true; } } else { if (reconcile(id, null, des)) reconciled = true; } } } return reconciled; } finally { descriptor.fLock.release(); } } private static boolean reconcile(String id, ICStorageElement newStorEl, ICProjectDescription des) throws CoreException { ICStorageElement storEl = des.getStorage(id, false); boolean modified = false; if(storEl != null){ if(newStorEl == null){ des.removeStorage(id); modified = true; } else { if(!newStorEl.equals(storEl)){ des.importStorage(id, newStorEl); modified = true; } } } else { if(newStorEl != null){ des.importStorage(id, newStorEl); modified = true; } } // Now storing the descriptor info directly in the Project Description. // Ensure that the setting is no longer stored in all the configurations for (ICConfigurationDescription cfgDes : des.getConfigurations()) { ICStorageElement el = cfgDes.getStorage(id, false); if (el != null) cfgDes.removeStorage(id); } return modified; } }