blob: d4251eb0f7697490efa8002b0d13c130b9139924 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
*******************************************************************************/
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);
}
}
}