| /******************************************************************************* |
| * Copyright (c) 2000, 2017 IBM 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 |
| * |
| *******************************************************************************/ |
| package org.eclipse.dltk.internal.core; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceChangeListener; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.ISafeRunnable; |
| import org.eclipse.core.runtime.SafeRunner; |
| import org.eclipse.dltk.core.DLTKCore; |
| import org.eclipse.dltk.core.IBuildpathEntry; |
| import org.eclipse.dltk.core.IElementChangedListener; |
| import org.eclipse.dltk.core.IModelElement; |
| import org.eclipse.dltk.core.IScriptModel; |
| import org.eclipse.dltk.core.IScriptProject; |
| import org.eclipse.dltk.core.ModelException; |
| import org.eclipse.dltk.internal.core.util.Util; |
| |
| /** |
| * Keep the global states used during script element delta processing. |
| */ |
| public class DeltaProcessingState implements IResourceChangeListener { |
| |
| /* |
| * Collection of listeners for model element deltas |
| */ |
| public IElementChangedListener[] elementChangedListeners = new IElementChangedListener[5]; |
| public int[] elementChangedListenerMasks = new int[5]; |
| public int elementChangedListenerCount = 0; |
| |
| /* |
| * Collection of pre script resource change listeners |
| */ |
| public IResourceChangeListener[] preResourceChangeListeners = new IResourceChangeListener[1]; |
| public int[] preResourceChangeEventMasks = new int[1]; |
| public int preResourceChangeListenerCount = 0; |
| |
| /* |
| * The delta processor for the current thread. |
| */ |
| private ThreadLocal<DeltaProcessor> deltaProcessors = new ThreadLocal<>(); |
| |
| /* A table from IPath (from a buildpath entry) to RootInfo */ |
| public HashMap roots = new HashMap(); |
| |
| /* |
| * A table from IPath (from a buildpath entry) to ArrayList of RootInfo Used |
| * when an IPath corresponds to more than one root |
| */ |
| public HashMap otherRoots = new HashMap(); |
| |
| /* |
| * A table from IPath (from a buildpath entry) to RootInfo from the last |
| * time the delta processor was invoked. |
| */ |
| public HashMap oldRoots = new HashMap(); |
| |
| /* |
| * A table from IPath (from a buildpath entry) to ArrayList of RootInfo from |
| * the last time the delta processor was invoked. Used when an IPath |
| * corresponds to more than one root |
| */ |
| public HashMap oldOtherRoots = new HashMap(); |
| |
| /* |
| * A table from IScriptProject to IScriptProject[] (the list of direct |
| * dependent of the key) |
| */ |
| public HashMap projectDependencies = new HashMap(); |
| |
| /* Whether the roots tables should be recomputed */ |
| public boolean rootsAreStale = true; |
| |
| /* Threads that are currently running initializeRoots() */ |
| private Set initializingThreads = Collections |
| .synchronizedSet(new HashSet()); |
| |
| /* A table from file system absoulte path (String) to timestamp (Long) */ |
| public PersistentTimeStampMap externalTimeStamps; |
| |
| /* A table from path (String) to timestamp (Long) */ |
| public PersistentTimeStampMap customTimeStamps; |
| |
| /* |
| * Map from IProject to BuildpathChange. Note these changes need to be kept |
| * on the delta processing state to ensure we don't loose them (see |
| * https://bugs.eclipse.org/bugs/show_bug.cgi?id=271102 Java model corrupt |
| * after switching target platform) |
| */ |
| private HashMap<IProject, BuildpathChange> buildpathChanges = new HashMap<>(); |
| |
| /* A table from ScriptProject to BuildpathValidation */ |
| private HashMap<ScriptProject, BuildpathValidation> buildpathValidations = new HashMap<>(); |
| |
| /* A table from ScriptProject to ProjectReferenceChange */ |
| private HashMap<ScriptProject, ProjectReferenceChange> projectReferenceChanges = new HashMap<>(); |
| |
| /* A table from JavaProject to ExternalFolderChange */ |
| private HashMap externalFolderChanges = new HashMap(); |
| |
| /** |
| * Workaround for bug 15168 circular errors not reported This is a cache of |
| * the projects before any project addition/deletion has started. |
| */ |
| private HashSet<String> scriptProjectNamesCache; |
| |
| /* |
| * A list of IModelElement used as a scope for external archives refresh |
| * during POST_CHANGE. This is null if no refresh is needed. |
| */ |
| private HashSet<IModelElement> externalElementsToRefresh; |
| |
| /* |
| * Need to clone defensively the listener information, in case some listener |
| * is reacting to some notification iteration by adding/changing/removing |
| * any of the other (for example, if it deregisters itself). |
| */ |
| public synchronized void addElementChangedListener( |
| IElementChangedListener listener, int eventMask) { |
| for (int i = 0; i < this.elementChangedListenerCount; i++) { |
| if (this.elementChangedListeners[i].equals(listener)) { |
| |
| // only clone the masks, since we could be in the middle of |
| // notifications and one listener decide to change |
| // any event mask of another listeners (yet not notified). |
| int cloneLength = this.elementChangedListenerMasks.length; |
| System.arraycopy(this.elementChangedListenerMasks, 0, |
| this.elementChangedListenerMasks = new int[cloneLength], |
| 0, cloneLength); |
| this.elementChangedListenerMasks[i] = eventMask; // could be |
| // different |
| return; |
| } |
| } |
| // may need to grow, no need to clone, since iterators will have cached |
| // original arrays and max boundary and we only add to the end. |
| int length; |
| if ((length = this.elementChangedListeners.length) == this.elementChangedListenerCount) { |
| System.arraycopy(this.elementChangedListeners, 0, |
| this.elementChangedListeners = new IElementChangedListener[length |
| * 2], |
| 0, length); |
| System.arraycopy(this.elementChangedListenerMasks, 0, |
| this.elementChangedListenerMasks = new int[length * 2], 0, |
| length); |
| } |
| this.elementChangedListeners[this.elementChangedListenerCount] = listener; |
| this.elementChangedListenerMasks[this.elementChangedListenerCount] = eventMask; |
| this.elementChangedListenerCount++; |
| } |
| |
| /* |
| * Adds the given element to the list of elements used as a scope for |
| * external jars refresh. |
| */ |
| public synchronized void addForRefresh(IModelElement externalElement) { |
| if (this.externalElementsToRefresh == null) { |
| this.externalElementsToRefresh = new HashSet(); |
| } |
| this.externalElementsToRefresh.add(externalElement); |
| } |
| |
| public void addPreResourceChangedListener(IResourceChangeListener listener, |
| int eventMask) { |
| for (int i = 0; i < this.preResourceChangeListenerCount; i++) { |
| if (this.preResourceChangeListeners[i].equals(listener)) { |
| this.preResourceChangeEventMasks[i] |= eventMask; |
| return; |
| } |
| } |
| // may need to grow, no need to clone, since iterators will have cached |
| // original arrays and max boundary and we only add to the end. |
| int length; |
| if ((length = this.preResourceChangeListeners.length) == this.preResourceChangeListenerCount) { |
| System.arraycopy(this.preResourceChangeListeners, 0, |
| this.preResourceChangeListeners = new IResourceChangeListener[length |
| * 2], |
| 0, length); |
| System.arraycopy(this.preResourceChangeEventMasks, 0, |
| this.preResourceChangeEventMasks = new int[length * 2], 0, |
| length); |
| } |
| this.preResourceChangeListeners[this.preResourceChangeListenerCount] = listener; |
| this.preResourceChangeEventMasks[this.preResourceChangeListenerCount] = eventMask; |
| this.preResourceChangeListenerCount++; |
| } |
| |
| public DeltaProcessor getDeltaProcessor() { |
| DeltaProcessor deltaProcessor = this.deltaProcessors.get(); |
| if (deltaProcessor != null) |
| return deltaProcessor; |
| deltaProcessor = new DeltaProcessor(this, |
| ModelManager.getModelManager()); |
| this.deltaProcessors.set(deltaProcessor); |
| return deltaProcessor; |
| } |
| |
| public synchronized BuildpathChange addBuildpathChange(IProject project, |
| IBuildpathEntry[] oldRawBuildpath, |
| IBuildpathEntry[] oldResolvedBuildpath) { |
| BuildpathChange change = this.buildpathChanges.get(project); |
| if (change == null) { |
| change = new BuildpathChange( |
| (ScriptProject) ModelManager.getModelManager().getModel() |
| .getScriptProject(project), |
| oldRawBuildpath, oldResolvedBuildpath); |
| this.buildpathChanges.put(project, change); |
| } else { |
| if (change.oldRawBuildpath == null) |
| change.oldRawBuildpath = oldRawBuildpath; |
| if (change.oldResolvedBuildpath == null) |
| change.oldResolvedBuildpath = oldResolvedBuildpath; |
| } |
| return change; |
| } |
| |
| public synchronized BuildpathChange getBuildpathChange(IProject project) { |
| return this.buildpathChanges.get(project); |
| } |
| |
| public synchronized HashMap<IProject, BuildpathChange> removeAllBuildpathChanges() { |
| final HashMap<IProject, BuildpathChange> result = this.buildpathChanges; |
| this.buildpathChanges = new HashMap<>( |
| result.size()); |
| return result; |
| } |
| |
| public synchronized BuildpathValidation addBuildpathValidation( |
| ScriptProject project) { |
| BuildpathValidation validation = this.buildpathValidations.get(project); |
| if (validation == null) { |
| validation = new BuildpathValidation(project); |
| this.buildpathValidations.put(project, validation); |
| } |
| return validation; |
| } |
| |
| public synchronized void addExternalFolderChange(ScriptProject project, |
| IBuildpathEntry[] oldResolvedClasspath) { |
| ExternalFolderChange change = (ExternalFolderChange) this.externalFolderChanges |
| .get(project); |
| if (change == null) { |
| change = new ExternalFolderChange(project, oldResolvedClasspath); |
| this.externalFolderChanges.put(project, change); |
| } |
| } |
| |
| public synchronized void addProjectReferenceChange(ScriptProject project, |
| IBuildpathEntry[] oldResolvedBuildpath) { |
| ProjectReferenceChange change = this.projectReferenceChanges |
| .get(project); |
| if (change == null) { |
| change = new ProjectReferenceChange(project, oldResolvedBuildpath); |
| this.projectReferenceChanges.put(project, change); |
| } |
| } |
| |
| public void initializeRoots() { |
| |
| // recompute root infos only if necessary |
| HashMap newRoots = null; |
| HashMap newOtherRoots = null; |
| HashMap newProjectDependencies = null; |
| if (this.rootsAreStale) { |
| Thread currentThread = Thread.currentThread(); |
| boolean addedCurrentThread = false; |
| try { |
| // if reentering initialization (through a container initializer |
| // for example) no need to compute roots again |
| // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=47213 |
| if (!this.initializingThreads.add(currentThread)) |
| return; |
| addedCurrentThread = true; |
| |
| // all buildpaths in the workspace are going to be resolved |
| // ensure that containers are initialized in one batch |
| ModelManager |
| .getModelManager().batchContainerInitializations = true; |
| |
| newRoots = new HashMap(); |
| newOtherRoots = new HashMap(); |
| newProjectDependencies = new HashMap(); |
| |
| IScriptModel model = ModelManager.getModelManager().getModel(); |
| IScriptProject[] projects; |
| try { |
| projects = model.getScriptProjects(); |
| } catch (ModelException e) { |
| // nothing can be done |
| return; |
| } |
| for (int i = 0, length = projects.length; i < length; i++) { |
| ScriptProject project = (ScriptProject) projects[i]; |
| IBuildpathEntry[] buildpath; |
| try { |
| buildpath = project.getResolvedBuildpath(); |
| } catch (ModelException e) { |
| // continue with next project |
| continue; |
| } |
| for (int j = 0, buildpathLength = buildpath.length; j < buildpathLength; j++) { |
| IBuildpathEntry entry = buildpath[j]; |
| if (entry |
| .getEntryKind() == IBuildpathEntry.BPE_PROJECT) { |
| IScriptProject key = model.getScriptProject( |
| entry.getPath().segment(0)); // TODO |
| // (jerome) |
| // reuse handle |
| IScriptProject[] dependents = (IScriptProject[]) newProjectDependencies |
| .get(key); |
| if (dependents == null) { |
| dependents = new IScriptProject[] { project }; |
| } else { |
| int dependentsLength = dependents.length; |
| System.arraycopy(dependents, 0, |
| dependents = new IScriptProject[dependentsLength |
| + 1], |
| 0, dependentsLength); |
| dependents[dependentsLength] = project; |
| } |
| newProjectDependencies.put(key, dependents); |
| continue; |
| } |
| |
| // root path |
| IPath path = entry.getPath(); |
| if (newRoots.get(path) == null) { |
| newRoots.put(path, new DeltaProcessor.RootInfo( |
| project, path, |
| ((BuildpathEntry) entry) |
| .fullInclusionPatternChars(), |
| ((BuildpathEntry) entry) |
| .fullExclusionPatternChars(), |
| entry.getEntryKind())); |
| } else { |
| ArrayList rootList = (ArrayList) newOtherRoots |
| .get(path); |
| if (rootList == null) { |
| rootList = new ArrayList(); |
| newOtherRoots.put(path, rootList); |
| } |
| rootList.add(new DeltaProcessor.RootInfo(project, |
| path, |
| ((BuildpathEntry) entry) |
| .fullInclusionPatternChars(), |
| ((BuildpathEntry) entry) |
| .fullExclusionPatternChars(), |
| entry.getEntryKind())); |
| } |
| } |
| } |
| } finally { |
| if (addedCurrentThread) { |
| this.initializingThreads.remove(currentThread); |
| } |
| } |
| } |
| synchronized (this) { |
| this.oldRoots = this.roots; |
| this.oldOtherRoots = this.otherRoots; |
| if (this.rootsAreStale && newRoots != null) { // double check again |
| this.roots = newRoots; |
| this.otherRoots = newOtherRoots; |
| this.projectDependencies = newProjectDependencies; |
| this.rootsAreStale = false; |
| } |
| } |
| } |
| |
| public synchronized BuildpathValidation[] removeBuildpathValidations() { |
| int length = this.buildpathValidations.size(); |
| if (length == 0) |
| return null; |
| BuildpathValidation[] validations = new BuildpathValidation[length]; |
| this.buildpathValidations.values().toArray(validations); |
| this.buildpathValidations.clear(); |
| return validations; |
| } |
| |
| public synchronized ProjectReferenceChange[] removeProjectReferenceChanges() { |
| int length = this.projectReferenceChanges.size(); |
| if (length == 0) |
| return null; |
| ProjectReferenceChange[] updates = new ProjectReferenceChange[length]; |
| this.projectReferenceChanges.values().toArray(updates); |
| this.projectReferenceChanges.clear(); |
| return updates; |
| } |
| |
| public synchronized Set<IModelElement> removeExternalElementsToRefresh() { |
| final Set<IModelElement> result = this.externalElementsToRefresh; |
| this.externalElementsToRefresh = null; |
| return result; |
| } |
| |
| public synchronized void removeElementChangedListener( |
| IElementChangedListener listener) { |
| |
| for (int i = 0; i < this.elementChangedListenerCount; i++) { |
| |
| if (this.elementChangedListeners[i].equals(listener)) { |
| |
| // need to clone defensively since we might be in the middle of |
| // listener notifications (#fire) |
| int length = this.elementChangedListeners.length; |
| IElementChangedListener[] newListeners = new IElementChangedListener[length]; |
| System.arraycopy(this.elementChangedListeners, 0, newListeners, |
| 0, i); |
| int[] newMasks = new int[length]; |
| System.arraycopy(this.elementChangedListenerMasks, 0, newMasks, |
| 0, i); |
| |
| // copy trailing listeners |
| int trailingLength = this.elementChangedListenerCount - i - 1; |
| if (trailingLength > 0) { |
| System.arraycopy(this.elementChangedListeners, i + 1, |
| newListeners, i, trailingLength); |
| System.arraycopy(this.elementChangedListenerMasks, i + 1, |
| newMasks, i, trailingLength); |
| } |
| |
| // update manager listener state (#fire need to iterate over |
| // original listeners through a local variable to hold onto |
| // the original ones) |
| this.elementChangedListeners = newListeners; |
| this.elementChangedListenerMasks = newMasks; |
| this.elementChangedListenerCount--; |
| return; |
| } |
| } |
| } |
| |
| public void removePreResourceChangedListener( |
| IResourceChangeListener listener) { |
| |
| for (int i = 0; i < this.preResourceChangeListenerCount; i++) { |
| |
| if (this.preResourceChangeListeners[i].equals(listener)) { |
| |
| // need to clone defensively since we might be in the middle of |
| // listener notifications (#fire) |
| int length = this.preResourceChangeListeners.length; |
| IResourceChangeListener[] newListeners = new IResourceChangeListener[length]; |
| int[] newEventMasks = new int[length]; |
| System.arraycopy(this.preResourceChangeListeners, 0, |
| newListeners, 0, i); |
| System.arraycopy(this.preResourceChangeEventMasks, 0, |
| newEventMasks, 0, i); |
| |
| // copy trailing listeners |
| int trailingLength = this.preResourceChangeListenerCount - i |
| - 1; |
| if (trailingLength > 0) { |
| System.arraycopy(this.preResourceChangeListeners, i + 1, |
| newListeners, i, trailingLength); |
| System.arraycopy(this.preResourceChangeEventMasks, i + 1, |
| newEventMasks, i, trailingLength); |
| } |
| |
| // update manager listener state (#fire need to iterate over |
| // original listeners through a local variable to hold onto |
| // the original ones) |
| this.preResourceChangeListeners = newListeners; |
| this.preResourceChangeEventMasks = newEventMasks; |
| this.preResourceChangeListenerCount--; |
| return; |
| } |
| } |
| } |
| |
| @Override |
| public void resourceChanged(final IResourceChangeEvent event) { |
| for (int i = 0; i < this.preResourceChangeListenerCount; i++) { |
| // wrap callbacks with Safe runnable for subsequent listeners to be |
| // called when some are causing grief |
| final IResourceChangeListener listener = this.preResourceChangeListeners[i]; |
| if ((this.preResourceChangeEventMasks[i] & event.getType()) != 0) |
| SafeRunner.run(new ISafeRunnable() { |
| @Override |
| public void handleException(Throwable exception) { |
| Util.log(exception, |
| "Exception occurred in listener of pre script resource change notification"); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void run() throws Exception { |
| listener.resourceChanged(event); |
| } |
| }); |
| } |
| try { |
| getDeltaProcessor().resourceChanged(event); |
| } finally { |
| // TODO (jerome) see 47631, may want to get rid of following so as |
| // to reuse delta processor ? |
| if (event.getType() == IResourceChangeEvent.POST_CHANGE) { |
| this.deltaProcessors.set(null); |
| } |
| } |
| |
| } |
| |
| public Map<IPath, Long> getExternalLibTimeStamps() { |
| if (this.externalTimeStamps == null) { |
| this.externalTimeStamps = new PersistentTimeStampMap( |
| getTimeStampsFile()); |
| } |
| return this.externalTimeStamps.getTimestamps(); |
| } |
| |
| public Map<IPath, Long> getCustomTimeStamps() { |
| if (this.customTimeStamps == null) { |
| this.customTimeStamps = new PersistentTimeStampMap( |
| getCustomTimeStampsFile()); |
| } |
| return this.customTimeStamps.getTimestamps(); |
| } |
| |
| public IScriptProject findProject(String name) { |
| if (getOldScriptProjectNames().contains(name)) |
| return ModelManager.getModelManager().getModel() |
| .getScriptProject(name); |
| return null; |
| } |
| |
| /* |
| * Workaround for bug 15168 circular errors not reported Returns the list |
| * ofscriptprojects before resource delta processing has started. |
| */ |
| public synchronized HashSet<String> getOldScriptProjectNames() { |
| if (this.scriptProjectNamesCache == null) { |
| HashSet<String> result = new HashSet<>(); |
| IScriptProject[] projects; |
| try { |
| projects = ModelManager.getModelManager().getModel() |
| .getScriptProjects(); |
| } catch (ModelException e) { |
| return this.scriptProjectNamesCache; |
| } |
| for (int i = 0, length = projects.length; i < length; i++) { |
| IScriptProject project = projects[i]; |
| result.add(project.getElementName()); |
| } |
| return this.scriptProjectNamesCache = result; |
| } |
| return this.scriptProjectNamesCache; |
| } |
| |
| public synchronized void resetOldScriptProjectNames() { |
| this.scriptProjectNamesCache = null; |
| } |
| |
| private File getTimeStampsFile() { |
| return DLTKCore.getDefault().getStateLocation() |
| .append("externalLibsTimeStamps").toFile(); //$NON-NLS-1$ |
| } |
| |
| private File getCustomTimeStampsFile() { |
| return DLTKCore.getDefault().getStateLocation() |
| .append("customTimeStamps").toFile(); //$NON-NLS-1$ |
| } |
| |
| public void saveExternalLibTimeStamps() throws CoreException { |
| if (this.externalTimeStamps != null) { |
| this.externalTimeStamps.save(); |
| } |
| if (this.customTimeStamps != null) { |
| this.customTimeStamps.save(); |
| } |
| } |
| |
| /* |
| * Update the roots that are affected by the addition or the removal of the |
| * given container resource. |
| */ |
| public synchronized void updateRoots(IPath containerPath, |
| IResourceDelta containerDelta, DeltaProcessor deltaProcessor) { |
| Map updatedRoots; |
| Map otherUpdatedRoots; |
| if (containerDelta.getKind() == IResourceDelta.REMOVED) { |
| updatedRoots = this.oldRoots; |
| otherUpdatedRoots = this.oldOtherRoots; |
| } else { |
| updatedRoots = this.roots; |
| otherUpdatedRoots = this.otherRoots; |
| } |
| Iterator iterator = updatedRoots.keySet().iterator(); |
| while (iterator.hasNext()) { |
| IPath path = (IPath) iterator.next(); |
| if (containerPath.isPrefixOf(path) && !containerPath.equals(path)) { |
| IResourceDelta rootDelta = containerDelta |
| .findMember(path.removeFirstSegments(1)); |
| if (rootDelta == null) |
| continue; |
| DeltaProcessor.RootInfo rootInfo = (DeltaProcessor.RootInfo) updatedRoots |
| .get(path); |
| |
| if (!rootInfo.project.getPath().isPrefixOf(path)) { // only |
| // consider |
| // roots |
| // that are |
| // not |
| // included |
| // in the |
| // container |
| deltaProcessor.updateCurrentDeltaAndIndex(rootDelta, |
| IModelElement.PROJECT_FRAGMENT, rootInfo); |
| } |
| |
| ArrayList rootList = (ArrayList) otherUpdatedRoots.get(path); |
| if (rootList != null) { |
| Iterator otherProjects = rootList.iterator(); |
| while (otherProjects.hasNext()) { |
| rootInfo = (DeltaProcessor.RootInfo) otherProjects |
| .next(); |
| if (!rootInfo.project.getPath().isPrefixOf(path)) { // only |
| // consider |
| // roots |
| // that |
| // are |
| // not |
| // included |
| // in |
| // the |
| // container |
| deltaProcessor.updateCurrentDeltaAndIndex(rootDelta, |
| IModelElement.PROJECT_FRAGMENT, rootInfo); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| public void updateProjectReferences(ScriptProject project, |
| IBuildpathEntry[] oldResolvedPath, boolean canChangeResources) |
| throws ModelException { |
| if (canChangeResources) { |
| new ProjectReferenceChange(project, oldResolvedPath) |
| .updateProjectReferencesIfNecessary(); |
| } else { |
| addProjectReferenceChange(project, oldResolvedPath); |
| } |
| } |
| } |