package org.rubypeople.rdt.internal.core;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.rubypeople.rdt.core.IElementChangedListener;
import org.rubypeople.rdt.core.ILoadpathEntry;
import org.rubypeople.rdt.core.IRubyElement;
import org.rubypeople.rdt.core.IRubyModel;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.RubyModelException;
import org.rubypeople.rdt.internal.core.DeltaProcessor.RootInfo;
import org.rubypeople.rdt.internal.core.util.CharOperation;
import org.rubypeople.rdt.internal.core.util.Util;
public class DeltaProcessingState implements IResourceChangeListener {
/*
* Collection of listeners for Ruby element deltas
*/
public IElementChangedListener[] elementChangedListeners = new IElementChangedListener[5];
public int[] elementChangedListenerMasks = new int[5];
public int elementChangedListenerCount = 0;
/*
* Collection of pre Ruby 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<DeltaProcessor>();
/* A table from IPath (from a classpath entry) to RootInfo */
public HashMap<IPath, RootInfo> roots = new HashMap<IPath, RootInfo>();
/* A table from IPath (from a classpath entry) to ArrayList of RootInfo
* Used when an IPath corresponds to more than one root */
public HashMap<IPath, ArrayList<RootInfo>> otherRoots = new HashMap<IPath, ArrayList<RootInfo>>();
/* A table from IPath (from a classpath entry) to RootInfo
* from the last time the delta processor was invoked. */
public HashMap<IPath, RootInfo> oldRoots = new HashMap<IPath, RootInfo>();
/* A table from IPath (from a classpath 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<IPath, ArrayList<RootInfo>> oldOtherRoots = new HashMap<IPath, ArrayList<RootInfo>>();
/* Whether the roots tables should be recomputed */
public boolean rootsAreStale = true;
/* Threads that are currently running initializeRoots() */
private Set<Thread> initializingThreads = Collections.synchronizedSet(new HashSet<Thread>());
/* A table from IRubyProject to IRubyProject[] (the list of direct dependent of the key) */
public HashMap<IRubyProject, IRubyProject[]> projectDependencies = new HashMap<IRubyProject, IRubyProject[]>();
public Hashtable<IPath, Long> externalTimeStamps;
public HashMap<RubyProject, ProjectUpdateInfo> projectUpdates = new HashMap<RubyProject, ProjectUpdateInfo>();
/**
* 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> rubyProjectNamesCache;
/*
* 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 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++;
}
public 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 DeltaProcessor getDeltaProcessor() {
DeltaProcessor deltaProcessor = (DeltaProcessor) this.deltaProcessors.get();
if (deltaProcessor != null) return deltaProcessor;
deltaProcessor = new DeltaProcessor(this, RubyModelManager.getRubyModelManager());
this.deltaProcessors.set(deltaProcessor);
return deltaProcessor;
}
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 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;
}
}
}
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() {
public void handleException(Throwable exception) {
Util
.log(exception,
"Exception occurred in listener of pre Ruby resource change notification"); //$NON-NLS-1$
}
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 void initializeRoots() {
// recompute root infos only if necessary
HashMap<IPath, RootInfo> newRoots = null;
HashMap<IPath, ArrayList<RootInfo>> newOtherRoots = null;
HashMap<IRubyProject, IRubyProject[]> 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 classpaths in the workspace are going to be resolved
// ensure that containers are initialized in one batch
RubyModelManager.getRubyModelManager().batchContainerInitializations = true;
newRoots = new HashMap<IPath, RootInfo>();
newOtherRoots = new HashMap<IPath, ArrayList<RootInfo>>();
newProjectDependencies = new HashMap<IRubyProject, IRubyProject[]>();
IRubyModel model = RubyModelManager.getRubyModelManager().getRubyModel();
IRubyProject[] projects;
try {
projects = model.getRubyProjects();
} catch (RubyModelException e) {
// nothing can be done
return;
}
for (int i = 0, length = projects.length; i < length; i++) {
RubyProject project = (RubyProject) projects[i];
ILoadpathEntry[] loadpath;
try {
loadpath = project.getResolvedLoadpath(true/*ignoreUnresolvedEntry*/, false/*don't generateMarkerOnError*/, false/*don't returnResolutionInProgress*/);
} catch (RubyModelException e) {
// continue with next project
continue;
}
for (int j= 0, loadpathLength = loadpath.length; j < loadpathLength; j++) {
ILoadpathEntry entry = loadpath[j];
if (entry.getEntryKind() == ILoadpathEntry.CPE_PROJECT) {
IRubyProject key = model.getRubyProject(entry.getPath().segment(0)); // TODO (jerome) reuse handle
IRubyProject[] dependents = (IRubyProject[]) newProjectDependencies.get(key);
if (dependents == null) {
dependents = new IRubyProject[] {project};
} else {
int dependentsLength = dependents.length;
System.arraycopy(dependents, 0, dependents = new IRubyProject[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, ((LoadpathEntry)entry).fullInclusionPatternChars(), ((LoadpathEntry)entry).fullExclusionPatternChars(), entry.getEntryKind()));
} else {
ArrayList<RootInfo> rootList = newOtherRoots.get(path);
if (rootList == null) {
rootList = new ArrayList<RootInfo>();
newOtherRoots.put(path, rootList);
}
rootList.add(new DeltaProcessor.RootInfo(project, path, ((LoadpathEntry)entry).fullInclusionPatternChars(), ((LoadpathEntry)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 Hashtable<IPath, Long> getExternalLibTimeStamps() {
if (this.externalTimeStamps == null) {
Hashtable<IPath, Long> timeStamps = new Hashtable<IPath, Long>();
File timestampsFile = getTimeStampsFile();
DataInputStream in = null;
try {
in = new DataInputStream(new BufferedInputStream(new FileInputStream(timestampsFile)));
int size = in.readInt();
while (size-- > 0) {
String key = in.readUTF();
long timestamp = in.readLong();
timeStamps.put(Path.fromPortableString(key), new Long(timestamp));
}
} catch (IOException e) {
if (timestampsFile.exists())
Util.log(e, "Unable to read external time stamps"); //$NON-NLS-1$
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// nothing we can do: ignore
}
}
}
this.externalTimeStamps = timeStamps;
}
return this.externalTimeStamps;
}
private File getTimeStampsFile() {
return RubyCore.getPlugin().getStateLocation().append("externalLibsTimeStamps").toFile(); //$NON-NLS-1$
}
public void saveExternalLibTimeStamps() throws CoreException {
if (this.externalTimeStamps == null) return;
File timestamps = getTimeStampsFile();
DataOutputStream out = null;
try {
out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(timestamps)));
out.writeInt(this.externalTimeStamps.size());
for (IPath key : this.externalTimeStamps.keySet()) {
out.writeUTF(key.toPortableString());
Long timestamp = this.externalTimeStamps.get(key);
out.writeLong(timestamp.longValue());
}
} catch (IOException e) {
IStatus status = new Status(IStatus.ERROR, RubyCore.PLUGIN_ID, IStatus.ERROR, "Problems while saving timestamps", e); //$NON-NLS-1$
throw new CoreException(status);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
// nothing we can do: ignore
}
}
}
}
public IRubyProject findRubyProject(String name) {
if (getOldRubyProjecNames().contains(name))
return RubyModelManager.getRubyModelManager().getRubyModel().getRubyProject(name);
return null;
}
/*
* Workaround for bug 15168 circular errors not reported
* Returns the list of java projects before resource delta processing
* has started.
*/
public synchronized HashSet<String> getOldRubyProjecNames() {
if (this.rubyProjectNamesCache == null) {
HashSet<String> result = new HashSet<String>();
IRubyProject[] projects;
try {
projects = RubyModelManager.getRubyModelManager().getRubyModel().getRubyProjects();
} catch (RubyModelException e) {
return this.rubyProjectNamesCache;
}
for (IRubyProject project : projects) {
result.add(project.getElementName());
}
return this.rubyProjectNamesCache = result;
}
return this.rubyProjectNamesCache;
}
public synchronized void resetOldRubyProjectNames() {
this.rubyProjectNamesCache = null;
}
public synchronized ProjectUpdateInfo[] removeAllProjectUpdates() {
int length = this.projectUpdates.size();
if (length == 0) return null;
ProjectUpdateInfo[] updates = new ProjectUpdateInfo[length];
this.projectUpdates.values().toArray(updates);
this.projectUpdates.clear();
return updates;
}
public void updateProjectReferences(RubyProject project, ILoadpathEntry[] oldResolvedPath, ILoadpathEntry[] newResolvedPath, ILoadpathEntry[] newRawPath, boolean canChangeResources) throws RubyModelException {
ProjectUpdateInfo info;
synchronized (this) {
info = (ProjectUpdateInfo) (canChangeResources ? this.projectUpdates.remove(project) /*remove possibly awaiting one*/ : this.projectUpdates.get(project));
if (info == null) {
info = new ProjectUpdateInfo();
info.project = project;
info.oldResolvedPath = oldResolvedPath;
if (!canChangeResources) {
this.projectUpdates.put(project, info);
}
} // else refresh new loadpath information
info.newResolvedPath = newResolvedPath;
info.newRawPath = newRawPath;
}
if (canChangeResources) {
info.updateProjectReferencesIfNecessary();
} // else project references will be updated on next PRE_BUILD notification
}
public static class ProjectUpdateInfo {
RubyProject project;
ILoadpathEntry[] oldResolvedPath;
ILoadpathEntry[] newResolvedPath;
ILoadpathEntry[] newRawPath;
/**
* Update projects references so that the build order is consistent with the classpath
*/
@SuppressWarnings("unchecked")
public void updateProjectReferencesIfNecessary() throws RubyModelException {
String[] oldRequired = this.oldResolvedPath == null ? CharOperation.NO_STRINGS : this.project.projectPrerequisites(this.oldResolvedPath);
if (this.newResolvedPath == null) {
if (this.newRawPath == null)
this.newRawPath = this.project.getRawLoadpath(true/*create markers*/, false/*don't log problems*/);
this.newResolvedPath =
this.project.getResolvedLoadpath(
this.newRawPath,
null/*no output*/,
true/*ignore unresolved entry*/,
true/*generate marker on error*/,
null/*no reverse map*/);
}
String[] newRequired = this.project.projectPrerequisites(this.newResolvedPath);
try {
IProject projectResource = this.project.getProject();
IProjectDescription description = projectResource.getDescription();
IProject[] projectReferences = description.getDynamicReferences();
HashSet<String> oldReferences = new HashSet<String>(projectReferences.length);
for (int i = 0; i < projectReferences.length; i++){
String projectName = projectReferences[i].getName();
oldReferences.add(projectName);
}
HashSet<String> newReferences = (HashSet<String>)oldReferences.clone();
for (int i = 0; i < oldRequired.length; i++){
String projectName = oldRequired[i];
newReferences.remove(projectName);
}
for (int i = 0; i < newRequired.length; i++){
String projectName = newRequired[i];
newReferences.add(projectName);
}
Iterator iter;
int newSize = newReferences.size();
checkIdentity: {
if (oldReferences.size() == newSize){
iter = newReferences.iterator();
while (iter.hasNext()){
if (!oldReferences.contains(iter.next())){
break checkIdentity;
}
}
return;
}
}
String[] requiredProjectNames = new String[newSize];
int index = 0;
iter = newReferences.iterator();
while (iter.hasNext()){
requiredProjectNames[index++] = (String)iter.next();
}
Util.sort(requiredProjectNames); // ensure that if changed, the order is consistent
IProject[] requiredProjectArray = new IProject[newSize];
IWorkspaceRoot wksRoot = projectResource.getWorkspace().getRoot();
for (int i = 0; i < newSize; i++){
requiredProjectArray[i] = wksRoot.getProject(requiredProjectNames[i]);
}
description.setDynamicReferences(requiredProjectArray);
projectResource.setDescription(description, null);
} catch(CoreException e){
// if (!ExternalRubyProject.EXTERNAL_PROJECT_NAME.equals(this.project.getElementName()))
throw new RubyModelException(e);
}
}
}
/*
* 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<IPath, RootInfo> updatedRoots;
Map<IPath, ArrayList<RootInfo>> otherUpdatedRoots;
if (containerDelta.getKind() == IResourceDelta.REMOVED) {
updatedRoots = this.oldRoots;
otherUpdatedRoots = this.oldOtherRoots;
} else {
updatedRoots = this.roots;
otherUpdatedRoots = this.otherRoots;
}
Iterator<IPath> 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, IRubyElement.SOURCE_FOLDER_ROOT, rootInfo);
}
ArrayList<RootInfo> rootList = otherUpdatedRoots.get(path);
if (rootList != null) {
Iterator<RootInfo> 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, IRubyElement.SOURCE_FOLDER_ROOT, rootInfo);
}
}
}
}
}
}
}