/* * Created on Jan 14, 2005 * */ 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.text.MessageFormat; 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.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.Map.Entry; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.ISaveContext; import org.eclipse.core.resources.ISaveParticipant; import org.eclipse.core.resources.ISavedState; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtension; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.PerformanceStats; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.Preferences; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.content.IContentTypeManager.ContentTypeChangeEvent; import org.eclipse.core.runtime.content.IContentTypeManager.IContentTypeChangeListener; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IPreferencesService; import org.eclipse.core.runtime.preferences.InstanceScope; import org.osgi.service.prefs.BackingStoreException; import org.rubypeople.rdt.core.ILoadpathAttribute; import org.rubypeople.rdt.core.ILoadpathContainer; import org.rubypeople.rdt.core.ILoadpathEntry; import org.rubypeople.rdt.core.IParent; import org.rubypeople.rdt.core.IProblemRequestor; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyModel; import org.rubypeople.rdt.core.IRubyModelMarker; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.ISourceFolder; import org.rubypeople.rdt.core.ISourceFolderRoot; import org.rubypeople.rdt.core.IType; import org.rubypeople.rdt.core.LoadpathContainerInitializer; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.core.WorkingCopyOwner; import org.rubypeople.rdt.core.compiler.CompilationParticipant; import org.rubypeople.rdt.core.compiler.IProblem; import org.rubypeople.rdt.core.search.IRubySearchScope; import org.rubypeople.rdt.internal.compiler.util.HashtableOfObjectToInt; import org.rubypeople.rdt.internal.core.buffer.BufferManager; import org.rubypeople.rdt.internal.core.builder.RubyBuilder; import org.rubypeople.rdt.internal.core.hierarchy.TypeHierarchy; import org.rubypeople.rdt.internal.core.parser.MarkerUtility; import org.rubypeople.rdt.internal.core.parser.RubyParser; import org.rubypeople.rdt.internal.core.search.AbstractSearchScope; import org.rubypeople.rdt.internal.core.search.RubyWorkspaceScope; import org.rubypeople.rdt.internal.core.search.indexing.IndexManager; import org.rubypeople.rdt.internal.core.util.Messages; import org.rubypeople.rdt.internal.core.util.Util; import org.rubypeople.rdt.internal.core.util.WeakHashSet; /** * @author cawilliams * */ public class RubyModelManager implements IContentTypeChangeListener, ISaveParticipant { private static final String BUFFER_MANAGER_DEBUG = RubyCore.PLUGIN_ID + "/debug/buffermanager"; //$NON-NLS-1$ private static final String TYPE_HIERARCHY_DEBUG = RubyCore.PLUGIN_ID + "/debug/typehierarchy"; //$NON-NLS-1$ private static final String RUBYMODEL_DEBUG = RubyCore.PLUGIN_ID + "/debug/rubymodel"; //$NON-NLS-1$ private static final String DELTA_DEBUG = RubyCore.PLUGIN_ID + "/debug/rubydelta"; //$NON-NLS-1$ private static final String DELTA_DEBUG_VERBOSE = RubyCore.PLUGIN_ID + "/debug/rubydelta/verbose"; //$NON-NLS-1$ private static final String POST_ACTION_DEBUG = RubyCore.PLUGIN_ID + "/debug/postaction"; //$NON-NLS-1$ private static final String BUILDER_DEBUG = RubyCore.PLUGIN_ID + "/debug/builder"; //$NON-NLS-1$ private static final String RUBY_PARSER_DEBUG_OPTION = RubyCore.PLUGIN_ID + "/rubyparser";//$NON-NLS-1$ private static final String MODEL_MANAGER_VERBOSE_OPTION = RubyCore.PLUGIN_ID + "/modelmanager";//$NON-NLS-1$ private static final String BUILDER_VERBOSE_OPTION = RubyCore.PLUGIN_ID + "/rubyBuilder";//$NON-NLS-1$ private static final String ENABLE_NEW_FORMATTER = RubyCore.PLUGIN_ID + "/formatter/enable_new"; //$NON-NLS-1$ public static final String DELTA_LISTENER_PERF = RubyCore.PLUGIN_ID + "/perf/rubydeltalistener"; //$NON-NLS-1$ public static final String RECONCILE_PERF = RubyCore.PLUGIN_ID + "/perf/reconcile"; //$NON-NLS-1$ private final static String INDEXED_SECONDARY_TYPES = "#@*_indexing secondary cache_*@#"; //$NON-NLS-1$ /** * Name of the extension point for contributing classpath variable initializers */ public static final String CPVARIABLE_INITIALIZER_EXTPOINT_ID = "loadpathVariableInitializer" ; //$NON-NLS-1$ /** * Name of the extension point for contributing classpath container initializers */ public static final String CPCONTAINER_INITIALIZER_EXTPOINT_ID = "loadpathContainerInitializer" ; //$NON-NLS-1$ /** * Loadpath variables pool */ public HashMap<String, IPath[]> variables = new HashMap<String, IPath[]>(5); public HashSet<String> variablesWithInitializer = new HashSet<String>(5); public HashMap<String, IPath[]> previousSessionVariables = new HashMap<String, IPath[]>(5); private ThreadLocal<HashSet<String>> variableInitializationInProgress = new ThreadLocal<HashSet<String>>(); /** * Loadpath containers pool */ public HashMap<IRubyProject, Map<IPath, ILoadpathContainer>> containers = new HashMap<IRubyProject, Map<IPath, ILoadpathContainer>>(5); public HashMap<IRubyProject, Map<IPath, ILoadpathContainer>> previousSessionContainers = new HashMap<IRubyProject, Map<IPath, ILoadpathContainer>>(5); private ThreadLocal<Map<IRubyProject, HashSet<IPath>>> containerInitializationInProgress = new ThreadLocal<Map<IRubyProject, HashSet<IPath>>>(); public boolean batchContainerInitializations = false; public HashMap<String, LoadpathContainerInitializer> containerInitializersCache = new HashMap<String, LoadpathContainerInitializer>(5); /** * The singleton manager */ private final static RubyModelManager MANAGER = new RubyModelManager(); /** * Holds the state used for delta processing. */ public DeltaProcessingState deltaState = new DeltaProcessingState(); public IndexManager indexManager = null; /** * Unique handle onto the RubyModel */ final RubyModel rubyModel = new RubyModel(); /* * Temporary cache of newly opened elements */ private ThreadLocal<HashMap> temporaryCache = new ThreadLocal<HashMap>(); /** * Set of elements which are out of sync with their buffers. */ protected HashSet<Openable> elementsOutOfSynchWithBuffers = new HashSet<Openable>(11); /* * A HashSet that contains the IRubyProject whose classpath is being * resolved. */ private ThreadLocal<HashSet<IRubyProject>> classpathsBeingResolved = new ThreadLocal<HashSet<IRubyProject>>(); /* * The unique workspace scope */ public RubyWorkspaceScope workspaceScope; /** * Infos cache. */ protected RubyModelCache cache = new RubyModelCache(); /** * Table from IProject to PerProjectInfo. NOTE: this object itself is used * as a lock to synchronize creation/removal of per project infos */ protected Map<IProject, PerProjectInfo> perProjectInfos = new HashMap<IProject, PerProjectInfo>(5); /** * Table from WorkingCopyOwner to a table of RubyScript (working copy * handle) to PerWorkingCopyInfo. NOTE: this object itself is used as a lock * to synchronize creation/removal of per working copy infos */ protected Map<WorkingCopyOwner, Map<RubyScript, PerWorkingCopyInfo>> perWorkingCopyInfos = new HashMap<WorkingCopyOwner, Map<RubyScript, PerWorkingCopyInfo>>(5); /** * A weak set of the known search scopes. */ protected WeakHashMap<AbstractSearchScope, Object> searchScopes = new WeakHashMap<AbstractSearchScope, Object>(); public static boolean CP_RESOLVE_VERBOSE = false; public static boolean ZIP_ACCESS_VERBOSE = false; public static boolean VERBOSE = false; // Preferences HashSet<String> optionNames = new HashSet<String>(20); Hashtable<String, String> optionsCache; public final IEclipsePreferences[] preferencesLookup = new IEclipsePreferences[2]; private WeakHashSet stringSymbols = new WeakHashSet(5); static final int PREF_INSTANCE = 0; static final int PREF_DEFAULT = 1; public static final IRubyScript[] NO_WORKING_COPY = new IRubyScript[0]; public final static String CP_VARIABLE_PREFERENCES_PREFIX = RubyCore.PLUGIN_ID+".loadpathVariable."; //$NON-NLS-1$ public final static String CP_CONTAINER_PREFERENCES_PREFIX = RubyCore.PLUGIN_ID+".loadpathContainer."; //$NON-NLS-1$ /** * Special value used for recognizing ongoing initialization and breaking initialization cycles */ public final static IPath[] VARIABLE_INITIALIZATION_IN_PROGRESS = new Path[] {new Path("Variable Initialization In Progress")}; //$NON-NLS-1$ public final static ILoadpathContainer CONTAINER_INITIALIZATION_IN_PROGRESS = new ILoadpathContainer() { public ILoadpathEntry[] getLoadpathEntries() { return null; } public String getDescription() { return "Container Initialization In Progress"; } //$NON-NLS-1$ public int getKind() { return 0; } public IPath getPath() { return null; } public String toString() { return getDescription(); } }; public final static String CP_ENTRY_IGNORE = "##<cp entry ignore>##"; //$NON-NLS-1$ public final static IPath[] CP_ENTRY_IGNORE_PATH = new Path[] {new Path(CP_ENTRY_IGNORE)}; private static final int VARIABLES_AND_CONTAINERS_FILE_VERSION = 2; public static boolean PERF_VARIABLE_INITIALIZER = false; public static boolean PERF_CONTAINER_INITIALIZER = false; private static final Object[] NO_PARTICIPANTS = new Object[0]; /** * Name of the extension point for contributing a compilation participant */ public static final String COMPILATION_PARTICIPANT_EXTPOINT_ID = "compilationParticipant" ; //$NON-NLS-1$ public class CompilationParticipants { private Object[] registeredParticipants = null; private HashSet<String> managedMarkerTypes; public CompilationParticipant[] getCompilationParticipants(IRubyProject project) { final Object[] participants = getRegisteredParticipants(); if (participants == NO_PARTICIPANTS) return null; int length = participants.length; final List<CompilationParticipant> result = new ArrayList<CompilationParticipant>(); for (int i = 0; i < length; i++) { if (participants[i] instanceof IConfigurationElement) { final IConfigurationElement configElement = (IConfigurationElement) participants[i]; SafeRunner.run(new ISafeRunnable() { public void handleException(Throwable exception) { Util.log(exception, "Exception occurred while creating compilation participant"); //$NON-NLS-1$ } public void run() throws Exception { Object executableExtension = configElement.createExecutableExtension("class"); //$NON-NLS-1$ CompilationParticipant participant = (CompilationParticipant) executableExtension; result.add(participant); } }); } else { CompilationParticipant participant = (CompilationParticipant) participants[i]; result.add(participant); } } if (result.isEmpty()) return null; List<CompilationParticipant> finalResult = new ArrayList<CompilationParticipant>(); for (CompilationParticipant participant : result) { if (participant != null && participant.isActive(project)) finalResult.add(participant); } return finalResult.toArray(new CompilationParticipant[finalResult.size()]); } public HashSet<String> managedMarkerTypes() { if (this.managedMarkerTypes == null) { // force extension points to be read getRegisteredParticipants(); } return this.managedMarkerTypes; } private synchronized Object[] getRegisteredParticipants() { if (this.registeredParticipants != null) { return this.registeredParticipants; } this.managedMarkerTypes = new HashSet<String>(); IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(RubyCore.PLUGIN_ID, COMPILATION_PARTICIPANT_EXTPOINT_ID); if (extension == null) return this.registeredParticipants = NO_PARTICIPANTS; final ArrayList<IConfigurationElement> modifyingEnv = new ArrayList<IConfigurationElement>(); final ArrayList<IConfigurationElement> creatingProblems = new ArrayList<IConfigurationElement>(); final ArrayList<IConfigurationElement> others = new ArrayList<IConfigurationElement>(); IExtension[] extensions = extension.getExtensions(); // for all extensions of this point... for(int i = 0; i < extensions.length; i++) { IConfigurationElement[] configElements = extensions[i].getConfigurationElements(); // for all config elements named "compilationParticipant" for(int j = 0; j < configElements.length; j++) { final IConfigurationElement configElement = configElements[j]; String elementName =configElement.getName(); if (!("compilationParticipant".equals(elementName))) { //$NON-NLS-1$ continue; } // add config element in the group it belongs to if ("true".equals(configElement.getAttribute("modifiesEnvironment"))) //$NON-NLS-1$ //$NON-NLS-2$ modifyingEnv.add(configElement); else if ("true".equals(configElement.getAttribute("createsProblems"))) //$NON-NLS-1$ //$NON-NLS-2$ creatingProblems.add(configElement); else others.add(configElement); // add managed marker types IConfigurationElement[] managedMarkers = configElement.getChildren("managedMarker"); //$NON-NLS-1$ for (int k = 0, length = managedMarkers.length; k < length; k++) { IConfigurationElement element = managedMarkers[k]; String markerType = element.getAttribute("markerType"); //$NON-NLS-1$ if (markerType != null) this.managedMarkerTypes.add(markerType); } } } int size = modifyingEnv.size() + creatingProblems.size() + others.size(); if (size == 0) return this.registeredParticipants = NO_PARTICIPANTS; // sort config elements in each group IConfigurationElement[] configElements = new IConfigurationElement[size]; int index = 0; index = sortParticipants(modifyingEnv, configElements, index); index = sortParticipants(creatingProblems, configElements, index); index = sortParticipants(others, configElements, index); return this.registeredParticipants = configElements; } private int sortParticipants(ArrayList group, IConfigurationElement[] configElements, int index) { int size = group.size(); if (size == 0) return index; Object[] elements = group.toArray(); Util.sort(elements, new Util.Comparer() { public int compare(Object a, Object b) { if (a == b) return 0; String id = ((IConfigurationElement) a).getAttribute("id"); //$NON-NLS-1$ if (id == null) return -1; IConfigurationElement[] requiredElements = ((IConfigurationElement) b).getChildren("requires"); //$NON-NLS-1$ for (int i = 0, length = requiredElements.length; i < length; i++) { IConfigurationElement required = requiredElements[i]; if (id.equals(required.getAttribute("id"))) //$NON-NLS-1$ return 1; } return -1; } }); for (int i = 0; i < size; i++) configElements[index+i] = (IConfigurationElement) elements[i]; return index + size; } } public final CompilationParticipants compilationParticipants = new CompilationParticipants(); /** * Update the classpath variable cache */ public static class EclipsePreferencesListener implements IEclipsePreferences.IPreferenceChangeListener { /** * @see org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener#preferenceChange(org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent) */ public void preferenceChange(IEclipsePreferences.PreferenceChangeEvent event) { // TODO Listen for loadpath changes! } } /** * Constructs a new RubyModelManager */ private RubyModelManager() { // singleton: prevent others from creating a new instance if (Platform.isRunning()) this.indexManager = new IndexManager(); } /** * Returns the singleton RubyModelManager */ public final static RubyModelManager getRubyModelManager() { return MANAGER; } /** * Initialize preferences lookups for JavaCore plugin. */ public void initializePreferences() { // Create lookups preferencesLookup[PREF_INSTANCE] = new InstanceScope().getNode(RubyCore.PLUGIN_ID); preferencesLookup[PREF_DEFAULT] = new DefaultScope().getNode(RubyCore.PLUGIN_ID); // Listen to instance preferences node removal from parent in order to refresh stored one IEclipsePreferences.INodeChangeListener listener = new IEclipsePreferences.INodeChangeListener() { public void added(IEclipsePreferences.NodeChangeEvent event) { // do nothing } public void removed(IEclipsePreferences.NodeChangeEvent event) { if (event.getChild() == preferencesLookup[PREF_INSTANCE]) { preferencesLookup[PREF_INSTANCE] = new InstanceScope().getNode(RubyCore.PLUGIN_ID); preferencesLookup[PREF_INSTANCE].addPreferenceChangeListener(new EclipsePreferencesListener()); } } }; ((IEclipsePreferences) preferencesLookup[PREF_INSTANCE].parent()).addNodeChangeListener(listener); preferencesLookup[PREF_INSTANCE].addPreferenceChangeListener(new EclipsePreferencesListener()); // Listen to default preferences node removal from parent in order to refresh stored one listener = new IEclipsePreferences.INodeChangeListener() { public void added(IEclipsePreferences.NodeChangeEvent event) { // do nothing } public void removed(IEclipsePreferences.NodeChangeEvent event) { if (event.getChild() == preferencesLookup[PREF_DEFAULT]) { preferencesLookup[PREF_DEFAULT] = new DefaultScope().getNode(RubyCore.PLUGIN_ID); } } }; ((IEclipsePreferences) preferencesLookup[PREF_DEFAULT].parent()).addNodeChangeListener(listener); } /** * Returns the info for the element. */ public synchronized Object getInfo(IRubyElement element) { HashMap tempCache = this.temporaryCache.get(); if (tempCache != null) { Object result = tempCache.get(element); if (result != null) { return result; } } return this.cache.getInfo(element); } /** * Remembers the given scope in a weak set * (so no need to remove it: it will be removed by the garbage collector) */ public void rememberScope(AbstractSearchScope scope) { // NB: The value has to be null so as to not create a strong reference on the scope this.searchScopes.put(scope, null); } /* * Removes all cached info for the given element (including all children) * from the cache. Returns the info for the given element, or null if it was * closed. */ public synchronized Object removeInfoAndChildren(RubyElement element) throws RubyModelException { Object info = this.cache.peekAtInfo(element); if (info != null) { element.closing(info); if (element instanceof IParent && info instanceof RubyElementInfo) { IRubyElement[] children = ((RubyElementInfo) info).getChildren(); for (int i = 0, size = children.length; i < size; ++i) { RubyElement child = (RubyElement) children[i]; child.close(); } } this.cache.removeInfo(element); return info; } return null; } /** * Returns the info for this element without disturbing the cache ordering. */ protected synchronized Object peekAtInfo(IRubyElement element) { HashMap tempCache = this.temporaryCache.get(); if (tempCache != null) { Object result = tempCache.get(element); if (result != null) { return result; } } return this.cache.peekAtInfo(element); } /* * Puts the infos in the given map (keys are IRubyElements and values are * RubyElementInfos) in the Ruby model cache in an atomic way. First checks * that the info for the opened element (or one of its ancestors) has not * been added to the cache. If it is the case, another thread has opened the * element (or one of its ancestors). So returns without updating the cache. */ protected synchronized void putInfos(IRubyElement openedElement, Map<IRubyElement, Object> newElements) { // remove children Object existingInfo = this.cache.peekAtInfo(openedElement); if (openedElement instanceof IParent && existingInfo instanceof RubyElementInfo) { IRubyElement[] children = ((RubyElementInfo) existingInfo).getChildren(); for (int i = 0, size = children.length; i < size; ++i) { RubyElement child = (RubyElement) children[i]; try { child.close(); } catch (RubyModelException e) { // ignore } } } Iterator<IRubyElement> iterator = newElements.keySet().iterator(); while (iterator.hasNext()) { IRubyElement element = iterator.next(); Object info = newElements.get(element); this.cache.putInfo(element, info); } } /** * Returns the temporary cache for newly opened elements for the current * thread. Creates it if not already created. */ public HashMap getTemporaryCache() { HashMap result = this.temporaryCache.get(); if (result == null) { result = new HashMap(); this.temporaryCache.set(result); } return result; } /* * Returns whether there is a temporary cache for the current thread. */ public boolean hasTemporaryCache() { return this.temporaryCache.get() != null; } /* * Reset project options stored in info cache. */ public void resetProjectOptions(RubyProject rubyProject) { synchronized (this.perProjectInfos) { // use the perProjectInfo // collection as its own lock IProject project = rubyProject.getProject(); PerProjectInfo info = this.perProjectInfos.get(project); if (info != null) { info.options = null; } } } /* * Reset project preferences stored in info cache. */ public void resetProjectPreferences(RubyProject rubyProject) { synchronized (this.perProjectInfos) { // use the perProjectInfo // collection as its own lock IProject project = rubyProject.getProject(); PerProjectInfo info = this.perProjectInfos.get(project); if (info != null) { info.preferences = null; } } } /* * Resets the temporary cache for newly created elements to null. */ public void resetTemporaryCache() { this.temporaryCache.set(null); } /** * Returns the handle to the active Ruby Model. */ public final RubyModel getRubyModel() { return this.rubyModel; } public static class PerWorkingCopyInfo implements IProblemRequestor { int useCount = 0; IRubyScript workingCopy; private IProblemRequestor problemRequestor; public PerWorkingCopyInfo(IRubyScript workingCopy, IProblemRequestor problemRequestor) { this.workingCopy = workingCopy; this.problemRequestor = problemRequestor; } public IRubyScript getWorkingCopy() { return this.workingCopy; } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("Info for "); //$NON-NLS-1$ buffer.append(((RubyElement) this.workingCopy).toString()); buffer.append("\nUse count = "); //$NON-NLS-1$ buffer.append(this.useCount); buffer.append("\nProblem requestor:\n "); //$NON-NLS-1$ buffer.append(this.problemRequestor); return buffer.toString(); } public void acceptProblem(IProblem problem) { // Don't accept the problem if a marker already exists for this same problem... try { IResource resource = workingCopy.getUnderlyingResource(); String markerType = IRubyModelMarker.RUBY_MODEL_PROBLEM_MARKER; if (problem.isTask()) { markerType = IRubyModelMarker.TASK_MARKER; } if (MarkerUtility.markerExists(resource, problem.getID(), problem.getSourceStart(), problem.getSourceEnd(), markerType)) return; } catch (RubyModelException e) { // ignore } catch (CoreException e) { // ignore } if (this.problemRequestor == null) return; this.problemRequestor.acceptProblem(problem); } public void beginReporting() { if (this.problemRequestor == null) return; this.problemRequestor.beginReporting(); } public void endReporting() { if (this.problemRequestor == null) return; this.problemRequestor.endReporting(); } public boolean isActive() { return this.problemRequestor != null && this.problemRequestor.isActive(); } } /** * @param script * @param create * @param recordUsage * @param problemRequestor * @param object * @return */ public PerWorkingCopyInfo getPerWorkingCopyInfo(RubyScript workingCopy, boolean create, boolean recordUsage, IProblemRequestor problemRequestor) { synchronized (this.perWorkingCopyInfos) { // use the // perWorkingCopyInfo // collection as its own // lock WorkingCopyOwner owner = workingCopy.owner; Map<RubyScript, PerWorkingCopyInfo> workingCopyToInfos = this.perWorkingCopyInfos.get(owner); if (workingCopyToInfos == null && create) { workingCopyToInfos = new HashMap<RubyScript, PerWorkingCopyInfo>(); this.perWorkingCopyInfos.put(owner, workingCopyToInfos); } PerWorkingCopyInfo info = workingCopyToInfos == null ? null : workingCopyToInfos.get(workingCopy); if (info == null && create) { info = new PerWorkingCopyInfo(workingCopy, problemRequestor); workingCopyToInfos.put(workingCopy, info); } if (info != null && recordUsage) info.useCount++; return info; } } /* * Discards the per working copy info for the given working copy (making it * a compilation unit) if its use count was 1. Otherwise, just decrement the * use count. If the working copy is primary, computes the delta between its * state and the original compilation unit and register it. Close the * working copy, its buffer and remove it from the shared working copy * table. Ignore if no per-working copy info existed. NOTE: it must NOT be * synchronized as it may interact with the element info cache (if useCount * is decremented to 0), see bug 50667. Returns the new use count (or -1 if * it didn't exist). */ public int discardPerWorkingCopyInfo(RubyScript workingCopy) throws RubyModelException { PerWorkingCopyInfo info = null; synchronized (this.perWorkingCopyInfos) { WorkingCopyOwner owner = workingCopy.owner; Map<RubyScript, PerWorkingCopyInfo> workingCopyToInfos = this.perWorkingCopyInfos.get(owner); if (workingCopyToInfos == null) return -1; info = workingCopyToInfos.get(workingCopy); if (info == null) return -1; if (--info.useCount == 0) { // remove per working copy info workingCopyToInfos.remove(workingCopy); if (workingCopyToInfos.isEmpty()) { this.perWorkingCopyInfos.remove(owner); } } } if (info.useCount == 0) { // info cannot be null here (check was done // above) // remove infos + close buffer (since no longer working copy) // outside the perWorkingCopyInfos lock (see bug 50667) removeInfoAndChildren(workingCopy); workingCopy.closeBuffer(); } return info.useCount; } /** * Returns the set of elements which are out of synch with their buffers. */ protected HashSet<Openable> getElementsOutOfSynchWithBuffers() { return this.elementsOutOfSynchWithBuffers; } /* * Returns the per-project info for the given project. If specified, create * the info if the info doesn't exist. */ public PerProjectInfo getPerProjectInfo(IProject project, boolean create) { synchronized (this.perProjectInfos) { // use the perProjectInfo // collection as its own lock PerProjectInfo info = this.perProjectInfos.get(project); if (info == null && create) { info = new PerProjectInfo(project); this.perProjectInfos.put(project, info); } return info; } } public void removePerProjectInfo(RubyProject rubyProject) { synchronized (this.perProjectInfos) { // use the perProjectInfo // collection as its own lock IProject project = rubyProject.getProject(); PerProjectInfo info = this.perProjectInfos.get(project); if (info != null) { this.perProjectInfos.remove(project); } } } public boolean isLoadpathBeingResolved(IRubyProject project) { return getLoadpathBeingResolved().contains(project); } private HashSet<IRubyProject> getLoadpathBeingResolved() { HashSet<IRubyProject> result = this.classpathsBeingResolved.get(); if (result == null) { result = new HashSet<IRubyProject>(); this.classpathsBeingResolved.set(result); } return result; } public static class PerProjectInfo { public IProject project; public Object savedState; public boolean triedRead; public ILoadpathEntry[] rawLoadpath; public ILoadpathEntry[] resolvedLoadpath; public Map<IPath, ILoadpathEntry> resolvedPathToRawEntries; // reverse map from resolved // path to raw entries public IPath outputLocation; public Hashtable<String, HashMap<?, IType>> secondaryTypes; // FIXME The ? is either String or IFile. It looks like the code expects both! EVIL!!1! public IEclipsePreferences preferences; public Hashtable<String, String> options; public PerProjectInfo(IProject project) { this.triedRead = false; this.savedState = null; this.project = project; } // updating raw loadpath need to flush obsoleted cached information // about resolved entries public synchronized void updateLoadpathInformation(ILoadpathEntry[] newRawLoadpath) { this.rawLoadpath = newRawLoadpath; this.resolvedLoadpath = null; this.resolvedPathToRawEntries = null; } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("Info for "); //$NON-NLS-1$ buffer.append(this.project.getFullPath()); buffer.append("\nRaw classpath:\n"); //$NON-NLS-1$ if (this.rawLoadpath == null) { buffer.append(" <null>\n"); //$NON-NLS-1$ } else { for (int i = 0, length = this.rawLoadpath.length; i < length; i++) { buffer.append(" "); //$NON-NLS-1$ buffer.append(this.rawLoadpath[i]); buffer.append('\n'); } } buffer.append("Resolved classpath:\n"); //$NON-NLS-1$ ILoadpathEntry[] resolvedCP = this.resolvedLoadpath; if (resolvedCP == null) { buffer.append(" <null>\n"); //$NON-NLS-1$ } else { for (int i = 0, length = resolvedCP.length; i < length; i++) { buffer.append(" "); //$NON-NLS-1$ buffer.append(resolvedCP[i]); buffer.append('\n'); } } buffer.append("Output location:\n "); //$NON-NLS-1$ if (this.outputLocation == null) { buffer.append("<null>"); //$NON-NLS-1$ } else { buffer.append(this.outputLocation); } return buffer.toString(); } public void rememberExternalLibTimestamps() { ILoadpathEntry[] classpath = this.resolvedLoadpath; if (classpath == null) return; Map<IPath, Long> externalTimeStamps = RubyModelManager.getRubyModelManager().deltaState.getExternalLibTimeStamps(); for (int i = 0, length = classpath.length; i < length; i++) { ILoadpathEntry entry = classpath[i]; if (entry.getEntryKind() == ILoadpathEntry.CPE_LIBRARY) { IPath path = entry.getPath(); if (externalTimeStamps.get(path) == null) { Object target = RubyModel.getTarget(path, true); if (target instanceof java.io.File) { long timestamp = DeltaProcessor.getTimeStamp((java.io.File)target); externalTimeStamps.put(path, new Long(timestamp)); } } } } } } /* * Returns the per-project info for the given project. If the info doesn't * exist, check for the project existence and create the info. @throws * RubyModelException if the project doesn't exist. */ public PerProjectInfo getPerProjectInfoCheckExistence(IProject project) throws RubyModelException { RubyModelManager.PerProjectInfo info = getPerProjectInfo(project, false /* * don't * create * info */); if (info == null) { if (!RubyProject.hasRubyNature(project)) { throw ((RubyProject) RubyCore .create(project)).newNotPresentException(); } info = getPerProjectInfo(project, true /* create info */); } return info; } public void setLoadpathBeingResolved(IRubyProject project, boolean classpathIsResolved) { if (classpathIsResolved) { getLoadpathBeingResolved().add(project); } else { getLoadpathBeingResolved().remove(project); } } public String getOption(String optionName) { if (RubyCore.CORE_ENCODING.equals(optionName)) { return RubyCore.getEncoding(); } String propertyName = optionName; if (this.optionNames.contains(propertyName)) { IPreferencesService service = Platform.getPreferencesService(); String value = service.get(optionName, null, this.preferencesLookup); return value == null ? null : value.trim(); } return null; } public Hashtable<String, String> getOptions() { // return cached options if already computed if (this.optionsCache != null) return new Hashtable<String, String>(this.optionsCache); // init Hashtable<String, String> options = new Hashtable<String, String>(10); IPreferencesService service = Platform.getPreferencesService(); // set options using preferences service lookup Iterator<String> iterator = optionNames.iterator(); while (iterator.hasNext()) { String propertyName = iterator.next(); String propertyValue = service.get(propertyName, null, this.preferencesLookup); if (propertyValue != null) { options.put(propertyName, propertyValue); } } // get encoding through resource plugin options.put(RubyCore.CORE_ENCODING, RubyCore.getEncoding()); // store built map in cache this.optionsCache = new Hashtable<String, String>(options); // return built map return options; } public DeltaProcessor getDeltaProcessor() { return this.deltaState.getDeltaProcessor(); } public void startup() throws CoreException { try { configurePluginDebugOptions(); // initialize Ruby model cache this.cache = new RubyModelCache(); // request state folder creation (workaround 19885) RubyCore.getPlugin().getStateLocation(); // Initialize eclipse preferences initializePreferences(); // Listen to preference changes Preferences.IPropertyChangeListener propertyListener = new Preferences.IPropertyChangeListener() { public void propertyChange(Preferences.PropertyChangeEvent event) { RubyModelManager.this.optionsCache = null; } }; RubyCore.getPlugin().getPluginPreferences().addPropertyChangeListener(propertyListener); // Listen to content-type changes Platform.getContentTypeManager().addContentTypeChangeListener(this); // retrieve variable values Job job = new Job("Loading variables and containers") { protected IStatus run(IProgressMonitor monitor) { try { long start = -1; if (VERBOSE) start = System.currentTimeMillis(); loadVariablesAndContainers(); if (VERBOSE) traceVariableAndContainers("Loaded", start); //$NON-NLS-1$ } catch (CoreException e) { return e.getStatus(); } return Status.OK_STATUS; } }; job.setSystem(true); job.setPriority(Job.SHORT); // process asap job.schedule(); final IWorkspace workspace = ResourcesPlugin.getWorkspace(); workspace.addResourceChangeListener( this.deltaState, /* update spec in RubyCore#addPreProcessingResourceChangedListener(...) if adding more event types */ IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.POST_BUILD | IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE | IResourceChangeEvent.PRE_CLOSE); job = new Job("Start Ruby Indexing") { protected IStatus run(IProgressMonitor monitor) { startIndexing(); // process deltas since last activated in indexer thread so that indexes are up-to-date. // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38658 Job processSavedState = new Job(Messages.savedState_jobName) { protected IStatus run(IProgressMonitor monitor) { try { // add save participant and process delta atomically // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59937 workspace.run( new IWorkspaceRunnable() { public void run(IProgressMonitor progress) throws CoreException { ISavedState savedState = workspace.addSaveParticipant(RubyCore.getRubyCore(), RubyModelManager.this); if (savedState != null) { // the event type coming from the saved state is always POST_AUTO_BUILD // force it to be POST_CHANGE so that the delta processor can handle it RubyModelManager.this.deltaState.getDeltaProcessor().overridenEventType = IResourceChangeEvent.POST_CHANGE; savedState.processResourceChangeEvents(RubyModelManager.this.deltaState); } } }, monitor); } catch (CoreException e) { return e.getStatus(); } return Status.OK_STATUS; } }; processSavedState.setSystem(true); processSavedState.setPriority(Job.SHORT); // process asap processSavedState.schedule(); return Status.OK_STATUS; } }; job.setSystem(true); job.setPriority(Job.SHORT); // process asap job.schedule(); } catch (RuntimeException e) { shutdown(); throw e; } } /** * Initiate the background indexing process. * This should be deferred after the plugin activation. */ private void startIndexing() { getIndexManager().reset(); } public void loadVariablesAndContainers() throws CoreException { // backward compatibility, load variables and containers from preferences into cache // TODO Erase these two line sthat were here for RDT backwards compatibility? loadVariablesAndContainers(getDefaultPreferences()); loadVariablesAndContainers(getInstancePreferences()); // load variables and containers from saved file into cache File file = getVariableAndContainersFile(); DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(new FileInputStream(file))); switch (in.readInt()) { case VARIABLES_AND_CONTAINERS_FILE_VERSION : new VariablesAndContainersLoadHelper(in).load(); break; } } catch (IOException e) { if (file.exists()) Util.log(e, "Unable to read variable and containers file"); //$NON-NLS-1$ } catch (RuntimeException e) { if (file.exists()) Util.log(e, "Unable to read variable and containers file (file is corrupt)"); //$NON-NLS-1$ } finally { if (in != null) { try { in.close(); } catch (IOException e) { // nothing we can do: ignore } } } // override persisted values for variables which have a registered initializer String[] registeredVariables = getRegisteredVariableNames(); for (int i = 0; i < registeredVariables.length; i++) { String varName = registeredVariables[i]; this.variables.put(varName, null); // reset variable, but leave its entry in the Map, so it will be part of variable names. } // override persisted values for containers which have a registered initializer containersReset(getRegisteredContainerIDs()); } public static void recreatePersistedContainer(String propertyName, String containerString, boolean addToContainerValues) { int containerPrefixLength = CP_CONTAINER_PREFERENCES_PREFIX.length(); int index = propertyName.indexOf('|', containerPrefixLength); if (containerString != null) containerString = containerString.trim(); if (index > 0) { String projectName = propertyName.substring(containerPrefixLength, index).trim(); IRubyProject project = getRubyModelManager().getRubyModel().getRubyProject(projectName); IPath containerPath = new Path(propertyName.substring(index+1).trim()); recreatePersistedContainer(project, containerPath, containerString, addToContainerValues); } } private static void recreatePersistedContainer(final IRubyProject project, final IPath containerPath, String containerString, boolean addToContainerValues) { if (!project.getProject().isAccessible()) return; // avoid leaking deleted project's persisted container if (containerString == null) { getRubyModelManager().containerPut(project, containerPath, null); } else { final ILoadpathEntry[] containerEntries = ((RubyProject) project).decodeLoadpath(containerString, false, false); if (containerEntries != null && containerEntries != RubyProject.INVALID_LOADPATH) { ILoadpathContainer container = new ILoadpathContainer() { public ILoadpathEntry[] getLoadpathEntries() { return containerEntries; } public String getDescription() { return "Persisted container ["+containerPath+" for project ["+ project.getElementName()+"]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ } public int getKind() { return 0; } public IPath getPath() { return containerPath; } public String toString() { return getDescription(); } }; if (addToContainerValues) { getRubyModelManager().containerPut(project, containerPath, container); } Map<IPath, ILoadpathContainer> projectContainers = getRubyModelManager().previousSessionContainers.get(project); if (projectContainers == null){ projectContainers = new HashMap<IPath, ILoadpathContainer>(1); getRubyModelManager().previousSessionContainers.put(project, projectContainers); } projectContainers.put(containerPath, container); } } } private File getVariableAndContainersFile() { return RubyCore.getPlugin().getStateLocation().append("variablesAndContainers.dat").toFile(); //$NON-NLS-1$ } private synchronized void containersReset(String[] containerIDs) { for (int i = 0; i < containerIDs.length; i++) { String containerID = containerIDs[i]; Iterator<IRubyProject> projectIterator = this.containers.keySet().iterator(); while (projectIterator.hasNext()){ IRubyProject project = projectIterator.next(); Map<IPath, ILoadpathContainer> projectContainers = this.containers.get(project); if (projectContainers != null){ Iterator<IPath> containerIterator = projectContainers.keySet().iterator(); while (containerIterator.hasNext()){ IPath containerPath = containerIterator.next(); if (containerPath.segment(0).equals(containerID)) { // registered container projectContainers.put(containerPath, null); // reset container value, but leave entry in Map } } } } } } /** * Returns the name of the variables for which an CP variable initializer is registered through an extension point */ public static String[] getRegisteredVariableNames(){ Plugin jdtCorePlugin = RubyCore.getPlugin(); if (jdtCorePlugin == null) return null; ArrayList<String> variableList = new ArrayList<String>(5); IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(RubyCore.PLUGIN_ID, RubyModelManager.CPVARIABLE_INITIALIZER_EXTPOINT_ID); if (extension != null) { IExtension[] extensions = extension.getExtensions(); for(int i = 0; i < extensions.length; i++){ IConfigurationElement [] configElements = extensions[i].getConfigurationElements(); for(int j = 0; j < configElements.length; j++){ String varAttribute = configElements[j].getAttribute("variable"); //$NON-NLS-1$ if (varAttribute != null) variableList.add(varAttribute); } } } String[] variableNames = new String[variableList.size()]; variableList.toArray(variableNames); return variableNames; } private void loadVariablesAndContainers(IEclipsePreferences preferences) { try { // only get variable from preferences not set to their default String[] propertyNames = preferences.keys(); int variablePrefixLength = CP_VARIABLE_PREFERENCES_PREFIX.length(); for (int i = 0; i < propertyNames.length; i++){ String propertyName = propertyNames[i]; if (propertyName.startsWith(CP_VARIABLE_PREFERENCES_PREFIX)){ String varName = propertyName.substring(variablePrefixLength); String propertyValue = preferences.get(propertyName, null); if (propertyValue != null) { String pathString = propertyValue.trim(); if (CP_ENTRY_IGNORE.equals(pathString)) { // cleanup old preferences preferences.remove(propertyName); continue; } // add variable to table String[] pathStrings = pathString.split(";"); IPath[] paths = new IPath[pathStrings.length]; for (int x = 0; x < paths.length; x++) { paths[x] = new Path(pathStrings[x]); } this.variables.put(varName, paths); this.previousSessionVariables.put(varName, paths); } } else if (propertyName.startsWith(CP_CONTAINER_PREFERENCES_PREFIX)){ String propertyValue = preferences.get(propertyName, null); if (propertyValue != null) { // cleanup old preferences preferences.remove(propertyName); // recreate container recreatePersistedContainer(propertyName, propertyValue, true/*add to container values*/); } } } } catch (BackingStoreException e1) { // TODO (frederic) see if it's necessary to report this failure... } } /** * Get default eclipse preference for JavaCore plugin. */ public IEclipsePreferences getDefaultPreferences() { return preferencesLookup[PREF_DEFAULT]; } /** * Returns the name of the container IDs for which an LP container initializer is registered through an extension point */ public static String[] getRegisteredContainerIDs(){ Plugin jdtCorePlugin = RubyCore.getPlugin(); if (jdtCorePlugin == null) return null; ArrayList<String> containerIDList = new ArrayList<String>(5); IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(RubyCore.PLUGIN_ID, RubyModelManager.CPCONTAINER_INITIALIZER_EXTPOINT_ID); if (extension != null) { IExtension[] extensions = extension.getExtensions(); for(int i = 0; i < extensions.length; i++){ IConfigurationElement [] configElements = extensions[i].getConfigurationElements(); for(int j = 0; j < configElements.length; j++){ String idAttribute = configElements[j].getAttribute("id"); //$NON-NLS-1$ if (idAttribute != null) containerIDList.add(idAttribute); } } } String[] containerIDs = new String[containerIDList.size()]; containerIDList.toArray(containerIDs); return containerIDs; } public void shutdown() { RubyCore rubyCore = RubyCore.getRubyCore(); rubyCore.savePluginPreferences(); IWorkspace workspace = ResourcesPlugin.getWorkspace(); workspace.removeResourceChangeListener(this.deltaState); workspace.removeSaveParticipant(rubyCore); if (this.indexManager != null){ // no more indexing this.indexManager.shutdown(); } // wait for the initialization job to finish try { Job.getJobManager().join(RubyCore.PLUGIN_ID, null); } catch (InterruptedException e) { // ignore } // Note: no need to close the Ruby model as this just removes Java // element infos from the Ruby model cache } /** * @see ISaveParticipant */ public void saving(ISaveContext context) throws CoreException { // save variable and container values on snapshot/full save // long start = -1; // if (VERBOSE) // start = System.currentTimeMillis(); saveVariablesAndContainers(); // if (VERBOSE) // traceVariableAndContainers("Saved", start); //$NON-NLS-1$ if (context.getKind() == ISaveContext.FULL_SAVE) { // will need delta since this save (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38658) context.needDelta(); // clean up indexes on workspace full save // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=52347) IndexManager manager = this.indexManager; if (manager != null // don't force initialization of workspace scope as we could be shutting down // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=93941) && this.workspaceScope != null) { manager.cleanUpIndexes(); } } IProject savedProject = context.getProject(); if (savedProject != null) { if (!RubyProject.hasRubyNature(savedProject)) return; // ignore PerProjectInfo info = getPerProjectInfo(savedProject, true /* create info */); saveState(info, context); info.rememberExternalLibTimestamps(); return; } ArrayList<IStatus> vStats= null; // lazy initialized ArrayList<PerProjectInfo> values = null; synchronized(this.perProjectInfos) { values = new ArrayList<PerProjectInfo>(this.perProjectInfos.values()); } if (values != null) { Iterator<PerProjectInfo> iterator = values.iterator(); while (iterator.hasNext()) { try { PerProjectInfo info = iterator.next(); saveState(info, context); info.rememberExternalLibTimestamps(); } catch (CoreException e) { if (vStats == null) vStats= new ArrayList<IStatus>(); vStats.add(e.getStatus()); } } } if (vStats != null) { IStatus[] stats= new IStatus[vStats.size()]; vStats.toArray(stats); throw new CoreException(new MultiStatus(RubyCore.PLUGIN_ID, IStatus.ERROR, stats, Messages.build_cannotSaveStates, null)); } // save external libs timestamps this.deltaState.saveExternalLibTimeStamps(); } private void saveVariablesAndContainers() throws CoreException { File file = getVariableAndContainersFile(); DataOutputStream out = null; try { out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); out.writeInt(VARIABLES_AND_CONTAINERS_FILE_VERSION); new VariablesAndContainersSaveHelper(out).save(); } catch (IOException e) { IStatus status = new Status(IStatus.ERROR, RubyCore.PLUGIN_ID, IStatus.ERROR, "Problems while saving variables and containers", e); //$NON-NLS-1$ throw new CoreException(status); } finally { if (out != null) { try { out.close(); } catch (IOException e) { // nothing we can do: ignore } } } } private void saveState(PerProjectInfo info, ISaveContext context) throws CoreException { // passed this point, save actions are non trivial if (context.getKind() == ISaveContext.SNAPSHOT) return; // save built state if (info.triedRead) saveBuiltState(info); } /** * Saves the built state for the project. */ private void saveBuiltState(PerProjectInfo info) throws CoreException { if (RubyBuilder.DEBUG) System.out.println(Messages.bind(Messages.build_saveStateProgress, info.project.getName())); File file = getSerializationFile(info.project); if (file == null) return; long t = System.currentTimeMillis(); try { DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); try { out.writeUTF(RubyCore.PLUGIN_ID); out.writeUTF("STATE"); //$NON-NLS-1$ if (info.savedState == null) { out.writeBoolean(false); } else { out.writeBoolean(true); RubyBuilder.writeState(info.savedState, out); } } finally { out.close(); } } catch (RuntimeException e) { try { file.delete(); } catch(SecurityException se) { // could not delete file: cannot do much more } throw new CoreException( new Status(IStatus.ERROR, RubyCore.PLUGIN_ID, Platform.PLUGIN_ERROR, Messages.bind(Messages.build_cannotSaveState, info.project.getName()), e)); } catch (IOException e) { try { file.delete(); } catch(SecurityException se) { // could not delete file: cannot do much more } throw new CoreException( new Status(IStatus.ERROR, RubyCore.PLUGIN_ID, Platform.PLUGIN_ERROR, Messages.bind(Messages.build_cannotSaveState, info.project.getName()), e)); } if (RubyBuilder.DEBUG) { t = System.currentTimeMillis() - t; System.out.println(Messages.bind(Messages.build_saveStateComplete, String.valueOf(t))); } } /** * Returns the File to use for saving and restoring the last built state for the given project. */ private File getSerializationFile(IProject project) { if (!project.exists()) return null; IPath workingLocation = project.getWorkingLocation(RubyCore.PLUGIN_ID); return workingLocation.append("state.dat").toFile(); //$NON-NLS-1$ } /** * Configure the plugin with respect to option settings defined in * ".options" file */ public void configurePluginDebugOptions() { if (RubyCore.getPlugin().isDebugging()) { String option = Platform.getDebugOption(BUFFER_MANAGER_DEBUG); if (option != null) BufferManager.VERBOSE = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(TYPE_HIERARCHY_DEBUG); if (option != null) TypeHierarchy.DEBUG = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(BUILDER_DEBUG); if (option != null) RubyBuilder.DEBUG = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(DELTA_DEBUG); if (option != null) DeltaProcessor.DEBUG = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(DELTA_DEBUG_VERBOSE); if (option != null) DeltaProcessor.VERBOSE = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(RUBYMODEL_DEBUG); if (option != null) RubyModelManager.VERBOSE = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(POST_ACTION_DEBUG); if (option != null) RubyModelOperation.POST_ACTION_VERBOSE = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(RUBY_PARSER_DEBUG_OPTION); if (option != null) RubyParser.setDebugging(option.equalsIgnoreCase("true")); //$NON-NLS-1$ option = Platform.getDebugOption(MODEL_MANAGER_VERBOSE_OPTION); if (option != null) RubyModelManager.VERBOSE = option.equalsIgnoreCase("true"); //$NON-NLS-1$ option = Platform.getDebugOption(BUILDER_VERBOSE_OPTION); if (option != null) RubyBuilder.setVerbose(option.equalsIgnoreCase("true")); //$NON-NLS-1$ // configure performance options if (PerformanceStats.ENABLED) { DeltaProcessor.PERF = PerformanceStats.isEnabled(DELTA_LISTENER_PERF); ReconcileWorkingCopyOperation.PERF = PerformanceStats.isEnabled(RECONCILE_PERF); } } } public void contentTypeChanged(ContentTypeChangeEvent event) { Util.resetRubyLikeExtensions(); } public static IRubyElement create(IResource resource, IRubyProject project) { if (resource == null) { return null; } int type = resource.getType(); switch (type) { case IResource.PROJECT : return RubyCore.create((IProject) resource); case IResource.FILE : return create((IFile) resource, project); case IResource.FOLDER : // System.err.println("Tried to create a RubyElement for: " + resource.getFullPath().toOSString()); return create((IFolder) resource, project); // return null; case IResource.ROOT : return RubyCore.create((IWorkspaceRoot) resource); default : return null; } } /** * Returns the package fragment or package fragment root corresponding to the given folder, * its parent or great parent being the given project. * or <code>null</code> if unable to associate the given folder with a Java element. * <p> * Note that a package fragment root is returned rather than a default package. * <p> * Creating a Java element has the side effect of creating and opening all of the * element's parents if they are not yet open. */ public static IRubyElement create(IFolder folder, IRubyProject project) { if (folder == null) { return null; } IRubyElement element; if (project == null) { project = RubyCore.create(folder.getProject()); element = determineIfOnLoadpath(folder, project); if (element == null) { // walk all projects and find one that have the given folder on the load path IRubyProject[] projects; try { projects = RubyModelManager.getRubyModelManager().getRubyModel().getRubyProjects(); } catch (RubyModelException e) { return null; } for (int i = 0, length = projects.length; i < length; i++) { project = projects[i]; element = determineIfOnLoadpath(folder, project); if (element != null) break; } } } else { element = determineIfOnLoadpath(folder, project); } return element; } private static IRubyElement determineIfOnLoadpath(IResource resource, IRubyProject project) { // TODO Actually take load paths into account IPath resourcePath = resource.getFullPath(); IPath rootPath = project.getPath(); if (rootPath.equals(resourcePath)) { return project.getSourceFolderRoot(resource); } else if (rootPath.isPrefixOf(resourcePath)) { SourceFolderRoot root =(SourceFolderRoot) ((RubyProject) project).getFolderSourceFolderRoot(rootPath); if (root == null) return null; IPath pkgPath = resourcePath.removeFirstSegments(rootPath.segmentCount()); if (resource.getType() == IResource.FILE) { // if the resource is a file, then remove the last segment which // is the file name in the package pkgPath = pkgPath.removeLastSegments(1); } String[] pkgName = pkgPath.segments(); return root.getSourceFolder(pkgName); } return null; } public static IRubyScript create(IFile file, IRubyProject project) { if (file == null) { return null; } if (project == null) { project = RubyCore.create(file.getProject()); } String name = file.getName(); if (org.rubypeople.rdt.internal.core.util.Util.isRubyLikeFileName(name) || org.rubypeople.rdt.internal.core.util.Util.isERBLikeFileName(name)) return createRubyScriptFrom(file, project); return null; } public static IRubyScript createRubyScriptFrom(IFile file, IRubyProject project) { if (file == null) return null; if (project == null) { project = RubyCore.create(file.getProject()); } ISourceFolder pkg = (ISourceFolder) determineIfOnLoadpath(file, project); if (pkg == null) { // not on classpath - make the root its folder, and a default package ISourceFolderRoot root = project.getSourceFolderRoot(file.getParent()); pkg = root.getSourceFolder(ISourceFolder.DEFAULT_PACKAGE_NAME); if (VERBOSE){ System.out.println("WARNING : creating unit element outside loadpath ("+ Thread.currentThread()+"): " + file.getFullPath()); //$NON-NLS-1$//$NON-NLS-2$ } } return pkg.getRubyScript(file.getName()); } /* * Returns all the working copies which have the given owner. * Adds the working copies of the primary owner if specified. * Returns null if it has none. */ public IRubyScript[] getWorkingCopies(WorkingCopyOwner owner, boolean addPrimary) { synchronized(this.perWorkingCopyInfos) { IRubyScript[] primaryWCs = addPrimary && owner != DefaultWorkingCopyOwner.PRIMARY ? getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, false) : null; Map<RubyScript, PerWorkingCopyInfo> workingCopyToInfos = this.perWorkingCopyInfos.get(owner); if (workingCopyToInfos == null) return primaryWCs; int primaryLength = primaryWCs == null ? 0 : primaryWCs.length; int size = workingCopyToInfos.size(); // note size is > 0 otherwise pathToPerWorkingCopyInfos would be null IRubyScript[] result = new IRubyScript[primaryLength + size]; int index = 0; if (primaryWCs != null) { for (int i = 0; i < primaryLength; i++) { IRubyScript primaryWorkingCopy = primaryWCs[i]; IRubyScript workingCopy = new RubyScript((SourceFolder) primaryWorkingCopy.getParent(), primaryWorkingCopy.getElementName(), owner); if (!workingCopyToInfos.containsKey(workingCopy)) result[index++] = primaryWorkingCopy; } if (index != primaryLength) System.arraycopy(result, 0, result = new IRubyScript[index+size], 0, index); } Iterator<PerWorkingCopyInfo> iterator = workingCopyToInfos.values().iterator(); while(iterator.hasNext()) { result[index++] = iterator.next().getWorkingCopy(); } return result; } } public synchronized String intern(String s) { // make sure to copy the string (so that it doesn't hold on the underlying char[] that might be much bigger than necessary) return (String) this.stringSymbols.add(new String(s)); // Note1: String#intern() cannot be used as on some VMs this prevents the string from being garbage collected // Note 2: Instead of using a WeakHashset, one could use a WeakHashMap with the following implementation // This would costs more per entry (one Entry object and one WeakReference more)) /* WeakReference reference = (WeakReference) this.symbols.get(s); String existing; if (reference != null && (existing = (String) reference.get()) != null) return existing; this.symbols.put(s, new WeakReference(s)); return s; */ } public void doneSaving(ISaveContext context) { // nothing to do } public void prepareToSave(ISaveContext context) throws CoreException { // nothing to do } public void rollback(ISaveContext context) { // nothing to do } public synchronized IPath[] variableGet(String variableName){ // check initialization in progress first HashSet<String> initializations = variableInitializationInProgress(); if (initializations.contains(variableName)) { return VARIABLE_INITIALIZATION_IN_PROGRESS; } IPath[] variablePath = this.variables.get(variableName); if (variablePath == null) return null; // Must make a copy of the return array, otherwise we somehow magically start messing up original variable mapping if/when we edit the array IPath[] copy = new IPath[variablePath.length]; System.arraycopy(variablePath, 0, copy, 0, variablePath.length); return copy; } /* * Returns the set of variable names that are being initialized in the current thread. */ private HashSet<String> variableInitializationInProgress() { HashSet<String> initializations = this.variableInitializationInProgress.get(); if (initializations == null) { initializations = new HashSet<String>(); this.variableInitializationInProgress.set(initializations); } return initializations; } /** * Returns a persisted container from previous session if any */ public IPath[] getPreviousSessionVariable(String variableName) { IPath[] previousPath = (IPath[])this.previousSessionVariables.get(variableName); if (previousPath != null){ if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPVariable INIT - reentering access to variable during its initialization, will see previous value\n" + //$NON-NLS-1$ " variable: "+ variableName + '\n' + //$NON-NLS-1$ " previous value: " + previousPath); //$NON-NLS-1$ new Exception("<Fake exception>").printStackTrace(System.out); //$NON-NLS-1$ } return previousPath; } return null; // break cycle } public synchronized void variablePut(String variableName, IPath[] variablePath){ // set/unset the initialization in progress HashSet<String> initializations = variableInitializationInProgress(); if (variablePath == VARIABLE_INITIALIZATION_IN_PROGRESS) { initializations.add(variableName); // do not write out intermediate initialization value return; } else { initializations.remove(variableName); // update cache - do not only rely on listener refresh if (variablePath == null) { // if path is null, record that the variable was removed to avoid asking the initializer to initialize it again // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=112609 this.variables.put(variableName, CP_ENTRY_IGNORE_PATH); } else { this.variables.put(variableName, variablePath); } // discard obsoleted information about previous session this.previousSessionVariables.remove(variableName); } } public ILoadpathContainer getLoadpathContainer(IPath containerPath, IRubyProject project) throws RubyModelException { ILoadpathContainer container = containerGet(project, containerPath); if (container == null) { if (this.batchContainerInitializations) { // avoid deep recursion while initializaing container on workspace restart // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=60437) this.batchContainerInitializations = false; return initializeAllContainers(project, containerPath); } return initializeContainer(project, containerPath); } return container; } ILoadpathContainer initializeContainer(IRubyProject project, IPath containerPath) throws RubyModelException { ILoadpathContainer container = null; final LoadpathContainerInitializer initializer = RubyCore.getLoadpathContainerInitializer(containerPath.segment(0)); if (initializer != null){ if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPContainer INIT - triggering initialization\n" + //$NON-NLS-1$ " project: " + project.getElementName() + '\n' + //$NON-NLS-1$ " container path: " + containerPath + '\n' + //$NON-NLS-1$ " initializer: " + initializer + '\n' + //$NON-NLS-1$ " invocation stack trace:"); //$NON-NLS-1$ new Exception("<Fake exception>").printStackTrace(System.out); //$NON-NLS-1$ } // PerformanceStats stats = null; // if(RubyModelManager.PERF_CONTAINER_INITIALIZER) { // stats = PerformanceStats.getStats(RubyModelManager.CONTAINER_INITIALIZER_PERF, this); // stats.startRun(containerPath + " of " + project.getPath()); //$NON-NLS-1$ // } containerPut(project, containerPath, CONTAINER_INITIALIZATION_IN_PROGRESS); // avoid initialization cycles boolean ok = false; try { // let OperationCanceledException go through // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59363) initializer.initialize(containerPath, project); // retrieve value (if initialization was successful) container = containerGet(project, containerPath); if (container == CONTAINER_INITIALIZATION_IN_PROGRESS) return null; // break cycle ok = true; } catch (CoreException e) { if (e instanceof RubyModelException) { throw (RubyModelException) e; } else { throw new RubyModelException(e); } } catch (RuntimeException e) { if (RubyModelManager.CP_RESOLVE_VERBOSE) { e.printStackTrace(); } throw e; } catch (Error e) { if (RubyModelManager.CP_RESOLVE_VERBOSE) { e.printStackTrace(); } throw e; } finally { // if(RubyModelManager.PERF_CONTAINER_INITIALIZER) { // stats.endRun(); // } if (!ok) { // just remove initialization in progress and keep previous session container so as to avoid a full build // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=92588 containerRemoveInitializationInProgress(project, containerPath); if (CP_RESOLVE_VERBOSE) { if (container == CONTAINER_INITIALIZATION_IN_PROGRESS) { Util.verbose( "CPContainer INIT - FAILED (initializer did not initialize container)\n" + //$NON-NLS-1$ " project: " + project.getElementName() + '\n' + //$NON-NLS-1$ " container path: " + containerPath + '\n' + //$NON-NLS-1$ " initializer: " + initializer); //$NON-NLS-1$ } else { Util.verbose( "CPContainer INIT - FAILED (see exception above)\n" + //$NON-NLS-1$ " project: " + project.getElementName() + '\n' + //$NON-NLS-1$ " container path: " + containerPath + '\n' + //$NON-NLS-1$ " initializer: " + initializer); //$NON-NLS-1$ } } } } if (CP_RESOLVE_VERBOSE){ StringBuffer buffer = new StringBuffer(); buffer.append("CPContainer INIT - after resolution\n"); //$NON-NLS-1$ buffer.append(" project: " + project.getElementName() + '\n'); //$NON-NLS-1$ buffer.append(" container path: " + containerPath + '\n'); //$NON-NLS-1$ if (container != null){ buffer.append(" container: "+container.getDescription()+" {\n"); //$NON-NLS-2$//$NON-NLS-1$ ILoadpathEntry[] entries = container.getLoadpathEntries(); if (entries != null){ for (int i = 0; i < entries.length; i++){ buffer.append(" " + entries[i] + '\n'); //$NON-NLS-1$ } } buffer.append(" }");//$NON-NLS-1$ } else { buffer.append(" container: {unbound}");//$NON-NLS-1$ } Util.verbose(buffer.toString()); } } else { if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPContainer INIT - no initializer found\n" + //$NON-NLS-1$ " project: " + project.getElementName() + '\n' + //$NON-NLS-1$ " container path: " + containerPath); //$NON-NLS-1$ } } return container; } private void containerRemoveInitializationInProgress(IRubyProject project, IPath containerPath) { HashSet<IPath> projectInitializations = containerInitializationInProgress(project); projectInitializations.remove(containerPath); if (projectInitializations.size() == 0) { Map<IRubyProject, HashSet<IPath>> initializations = this.containerInitializationInProgress.get(); initializations.remove(project); } } public synchronized void containerPut(IRubyProject project, IPath containerPath, ILoadpathContainer container){ // set/unset the initialization in progress if (container == CONTAINER_INITIALIZATION_IN_PROGRESS) { HashSet<IPath> projectInitializations = containerInitializationInProgress(project); projectInitializations.add(containerPath); // do not write out intermediate initialization value return; } else { containerRemoveInitializationInProgress(project, containerPath); Map<IPath, ILoadpathContainer> projectContainers = this.containers.get(project); if (projectContainers == null){ projectContainers = new HashMap<IPath, ILoadpathContainer>(1); this.containers.put(project, projectContainers); } if (container == null) { projectContainers.remove(containerPath); } else { projectContainers.put(containerPath, container); } // discard obsoleted information about previous session Map<IPath, ILoadpathContainer> previousContainers = this.previousSessionContainers.get(project); if (previousContainers != null){ previousContainers.remove(containerPath); } } // container values are persisted in preferences during save operations, see #saving(ISaveContext) } /* * Initialize all container at the same time as the given container. * Return the container for the given path and project. */ private ILoadpathContainer initializeAllContainers(IRubyProject javaProjectToInit, IPath containerToInit) throws RubyModelException { if (CP_RESOLVE_VERBOSE) { Util.verbose( "CPContainer INIT - batching containers initialization\n" + //$NON-NLS-1$ " project to init: " + javaProjectToInit.getElementName() + '\n' + //$NON-NLS-1$ " container path to init: " + containerToInit); //$NON-NLS-1$ } // collect all container paths final HashMap<IRubyProject, HashSet<IPath>> allContainerPaths = new HashMap<IRubyProject, HashSet<IPath>>(); IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (int i = 0, length = projects.length; i < length; i++) { IProject project = projects[i]; if (!RubyProject.hasRubyNature(project)) continue; IRubyProject javaProject = new RubyProject(project, getRubyModel()); HashSet<IPath> paths = null; ILoadpathEntry[] rawClasspath = javaProject.getRawLoadpath(); for (int j = 0, length2 = rawClasspath.length; j < length2; j++) { ILoadpathEntry entry = rawClasspath[j]; IPath path = entry.getPath(); if (entry.getEntryKind() == ILoadpathEntry.CPE_CONTAINER && containerGet(javaProject, path) == null) { if (paths == null) { paths = new HashSet<IPath>(); allContainerPaths.put(javaProject, paths); } paths.add(path); } } /* TODO (frederic) put back when JDT/UI dummy project will be thrown away... * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=97524 * if (javaProject.equals(javaProjectToInit)) { if (paths == null) { paths = new HashSet(); allContainerPaths.put(javaProject, paths); } paths.add(containerToInit); } */ } // TODO (frederic) remove following block when JDT/UI dummy project will be thrown away... HashSet<IPath> containerPaths = allContainerPaths.get(javaProjectToInit); if (containerPaths == null) { containerPaths = new HashSet<IPath>(); allContainerPaths.put(javaProjectToInit, containerPaths); } containerPaths.add(containerToInit); // end block // mark all containers as being initialized this.containerInitializationInProgress.set(allContainerPaths); // initialize all containers boolean ok = false; try { // if possible run inside an IWokspaceRunnable with AVOID_UPATE to avoid unwanted builds // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=118507) IWorkspaceRunnable runnable = new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { Set<IRubyProject> keys = allContainerPaths.keySet(); int length = keys.size(); IRubyProject[] javaProjects = new IRubyProject[length]; // clone as the following will have a side effect keys.toArray(javaProjects); for (int i = 0; i < length; i++) { IRubyProject javaProject = javaProjects[i]; HashSet<IPath> pathSet = allContainerPaths.get(javaProject); if (pathSet == null) continue; int length2 = pathSet.size(); IPath[] paths = new IPath[length2]; pathSet.toArray(paths); // clone as the following will have a side effect for (int j = 0; j < length2; j++) { IPath path = paths[j]; initializeContainer(javaProject, path); } } } }; IWorkspace workspace = ResourcesPlugin.getWorkspace(); if (workspace.isTreeLocked()) runnable.run(null/*no progress available*/); else workspace.run( runnable, null/*don't take any lock*/, IWorkspace.AVOID_UPDATE, null/*no progress available here*/); ok = true; } catch (CoreException e) { // ignore Util.log(e, "Exception while initializing all containers"); //$NON-NLS-1$ } finally { if (!ok) { // if we're being traversed by an exception, ensure that that containers are // no longer marked as initialization in progress // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=66437) this.containerInitializationInProgress.set(null); } } return containerGet(javaProjectToInit, containerToInit); } public synchronized ILoadpathContainer containerGet(IRubyProject project, IPath containerPath) { // check initialization in progress first HashSet<IPath> projectInitializations = containerInitializationInProgress(project); if (projectInitializations.contains(containerPath)) { return CONTAINER_INITIALIZATION_IN_PROGRESS; } Map<IPath, ILoadpathContainer> projectContainers = this.containers.get(project); if (projectContainers == null){ return null; } ILoadpathContainer container = projectContainers.get(containerPath); return container; } /* * Returns the set of container paths for the given project that are being initialized in the current thread. */ private HashSet<IPath> containerInitializationInProgress(IRubyProject project) { Map<IRubyProject, HashSet<IPath>> initializations = this.containerInitializationInProgress.get(); if (initializations == null) { initializations = new HashMap<IRubyProject, HashSet<IPath>>(); this.containerInitializationInProgress.set(initializations); } HashSet<IPath> projectInitializations = initializations.get(project); if (projectInitializations == null) { projectInitializations = new HashSet<IPath>(); initializations.put(project, projectInitializations); } return projectInitializations; } /** * Returns a persisted container from previous session if any. Note that it is not the original container from previous * session (i.e. it did not get serialized) but rather a summary of its entries recreated for CP initialization purpose. * As such it should not be stored into container caches. */ public ILoadpathContainer getPreviousSessionContainer(IPath containerPath, IRubyProject project) { Map<IPath, ILoadpathContainer> previousContainerValues = this.previousSessionContainers.get(project); if (previousContainerValues != null){ ILoadpathContainer previousContainer = (ILoadpathContainer)previousContainerValues.get(containerPath); if (previousContainer != null) { if (RubyModelManager.CP_RESOLVE_VERBOSE){ StringBuffer buffer = new StringBuffer(); buffer.append("CPContainer INIT - reentering access to project container during its initialization, will see previous value\n"); //$NON-NLS-1$ buffer.append(" project: " + project.getElementName() + '\n'); //$NON-NLS-1$ buffer.append(" container path: " + containerPath + '\n'); //$NON-NLS-1$ buffer.append(" previous value: "); //$NON-NLS-1$ buffer.append(previousContainer.getDescription()); buffer.append(" {\n"); //$NON-NLS-1$ ILoadpathEntry[] entries = previousContainer.getLoadpathEntries(); if (entries != null){ for (int j = 0; j < entries.length; j++){ buffer.append(" "); //$NON-NLS-1$ buffer.append(entries[j]); buffer.append('\n'); } } buffer.append(" }"); //$NON-NLS-1$ Util.verbose(buffer.toString()); new Exception("<Fake exception>").printStackTrace(System.out); //$NON-NLS-1$ } return previousContainer; } } return null; // break cycle if none found } /* * The given project is being removed. Remove all containers for this project from the cache. */ public void containerRemove(IRubyProject project) { Map<IRubyProject, HashSet<IPath>> initializations = this.containerInitializationInProgress.get(); if (initializations != null) { initializations.remove(project); } this.containers.remove(project); } /** * Sets the last built state for the given project, or null to reset it. */ public void setLastBuiltState(IProject project, Object state) { if (RubyProject.hasRubyNature(project)) { // should never be requested on non-Ruby projects PerProjectInfo info = getPerProjectInfo(project, true /*create if missing*/); info.triedRead = true; // no point trying to re-read once using setter info.savedState = state; } if (state == null) { // delete state file to ensure a full build happens if the workspace crashes try { File file = getSerializationFile(project); if (file != null && file.exists()) file.delete(); } catch(SecurityException se) { // could not delete file: cannot do much more } } } /* * Optimize startup case where a container for 1 project is initialized at a time with the same entries as on shutdown. */ public boolean containerPutIfInitializingWithSameEntries(IPath containerPath, IRubyProject[] projects, ILoadpathContainer[] respectiveContainers) { int projectLength = projects.length; if (projectLength != 1) return false; final ILoadpathContainer container = respectiveContainers[0]; if (container == null) return false; IRubyProject project = projects[0]; if (!containerInitializationInProgress(project).contains(containerPath)) return false; ILoadpathContainer previousSessionContainer = getPreviousSessionContainer(containerPath, project); final ILoadpathEntry[] newEntries = container.getLoadpathEntries(); if (previousSessionContainer == null) if (newEntries.length == 0) { containerPut(project, containerPath, container); return true; } else { return false; } final ILoadpathEntry[] oldEntries = previousSessionContainer.getLoadpathEntries(); if (oldEntries.length != newEntries.length) return false; for (int i = 0, length = newEntries.length; i < length; i++) { if (!newEntries[i].equals(oldEntries[i])) { if (CP_RESOLVE_VERBOSE) { Util.verbose( "CPContainer SET - missbehaving container\n" + //$NON-NLS-1$ " container path: " + containerPath + '\n' + //$NON-NLS-1$ " projects: {" +//$NON-NLS-1$ org.rubypeople.rdt.core.util.Util.toString( projects, new org.rubypeople.rdt.core.util.Util.Displayable(){ public String displayString(Object o) { return ((IRubyProject) o).getElementName(); } }) + "}\n values on previous session: {\n" +//$NON-NLS-1$ org.rubypeople.rdt.core.util.Util.toString( respectiveContainers, new org.rubypeople.rdt.core.util.Util.Displayable(){ public String displayString(Object o) { StringBuffer buffer = new StringBuffer(" "); //$NON-NLS-1$ if (o == null) { buffer.append("<null>"); //$NON-NLS-1$ return buffer.toString(); } buffer.append(container.getDescription()); buffer.append(" {\n"); //$NON-NLS-1$ for (int j = 0; j < oldEntries.length; j++){ buffer.append(" "); //$NON-NLS-1$ buffer.append(oldEntries[j]); buffer.append('\n'); } buffer.append(" }"); //$NON-NLS-1$ return buffer.toString(); } }) + "}\n new values: {\n" +//$NON-NLS-1$ org.rubypeople.rdt.core.util.Util.toString( respectiveContainers, new org.rubypeople.rdt.core.util.Util.Displayable(){ public String displayString(Object o) { StringBuffer buffer = new StringBuffer(" "); //$NON-NLS-1$ if (o == null) { buffer.append("<null>"); //$NON-NLS-1$ return buffer.toString(); } buffer.append(container.getDescription()); buffer.append(" {\n"); //$NON-NLS-1$ for (int j = 0; j < newEntries.length; j++){ buffer.append(" "); //$NON-NLS-1$ buffer.append(newEntries[j]); buffer.append('\n'); } buffer.append(" }"); //$NON-NLS-1$ return buffer.toString(); } }) + "\n }"); //$NON-NLS-1$ } return false; } } containerPut(project, containerPath, container); return true; } /* * Internal updating of a variable values (null path meaning removal), allowing to change multiple variable values at once. */ public void updateVariableValues( String[] variableNames, IPath[][] variablePaths, boolean updatePreferences, IProgressMonitor monitor) throws RubyModelException { if (monitor != null && monitor.isCanceled()) return; if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPVariable SET - setting variables\n" + //$NON-NLS-1$ " variables: " + org.rubypeople.rdt.core.util.Util.toString(variableNames) + '\n' +//$NON-NLS-1$ " values: " + org.rubypeople.rdt.core.util.Util.toString(variablePaths)); //$NON-NLS-1$ } if (variablePutIfInitializingWithSameValue(variableNames, variablePaths)) return; int varLength = variableNames.length; // gather classpath information for updating final HashMap<RubyProject, ILoadpathEntry[]> affectedProjectClasspaths = new HashMap<RubyProject, ILoadpathEntry[]>(5); IRubyModel model = getRubyModel(); // filter out unmodified variables int discardCount = 0; for (int i = 0; i < varLength; i++){ String variableName = variableNames[i]; IPath[] oldPath = this.variableGet(variableName); // if reentering will provide previous session value if (oldPath == VARIABLE_INITIALIZATION_IN_PROGRESS){ // IPath previousPath = (IPath)this.previousSessionVariables.get(variableName); // if (previousPath != null){ // if (CP_RESOLVE_VERBOSE){ // Util.verbose( // "CPVariable INIT - reentering access to variable during its initialization, will see previous value\n" + // " variable: "+ variableName + '\n' + // " previous value: " + previousPath); // } // this.variablePut(variableName, previousPath); // replace value so reentering calls are seeing old value // } oldPath = null; //33695 - cannot filter out restored variable, must update affected project to reset cached CP } if (oldPath != null && oldPath.equals(variablePaths[i])){ variableNames[i] = null; discardCount++; } } if (discardCount > 0){ if (discardCount == varLength) return; int changedLength = varLength - discardCount; String[] changedVariableNames = new String[changedLength]; IPath[][] changedVariablePaths = new IPath[changedLength][]; for (int i = 0, index = 0; i < varLength; i++){ if (variableNames[i] != null){ changedVariableNames[index] = variableNames[i]; changedVariablePaths[index] = variablePaths[i]; index++; } } variableNames = changedVariableNames; variablePaths = changedVariablePaths; varLength = changedLength; } if (monitor != null && monitor.isCanceled()) return; if (model != null) { IRubyProject[] projects = model.getRubyProjects(); nextProject : for (int i = 0, projectLength = projects.length; i < projectLength; i++){ RubyProject project = (RubyProject) projects[i]; // check to see if any of the modified variables is present on the loadpath ILoadpathEntry[] classpath = project.getRawLoadpath(); for (int j = 0, cpLength = classpath.length; j < cpLength; j++){ ILoadpathEntry entry = classpath[j]; for (int k = 0; k < varLength; k++){ String variableName = variableNames[k]; if (entry.getEntryKind() == ILoadpathEntry.CPE_VARIABLE){ if (variableName.equals(entry.getPath().segment(0))){ affectedProjectClasspaths.put(project, project.getResolvedLoadpath(true/*ignoreUnresolvedEntry*/, false/*don't generateMarkerOnError*/, false/*don't returnResolutionInProgress*/)); continue nextProject; } } } } } } // update variables for (int i = 0; i < varLength; i++){ variablePut(variableNames[i], variablePaths[i]); if (updatePreferences) variablePreferencesPut(variableNames[i], variablePaths[i]); } final String[] dbgVariableNames = variableNames; // update affected project loadpaths if (!affectedProjectClasspaths.isEmpty()) { try { final boolean canChangeResources = !ResourcesPlugin.getWorkspace().isTreeLocked(); RubyCore.run( new IWorkspaceRunnable() { public void run(IProgressMonitor progressMonitor) throws CoreException { // propagate loadpath change Iterator<RubyProject> projectsToUpdate = affectedProjectClasspaths.keySet().iterator(); while (projectsToUpdate.hasNext()) { if (progressMonitor != null && progressMonitor.isCanceled()) return; RubyProject affectedProject = projectsToUpdate.next(); if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPVariable SET - updating affected project due to setting variables\n" + //$NON-NLS-1$ " project: " + affectedProject.getElementName() + '\n' + //$NON-NLS-1$ " variables: " + org.rubypeople.rdt.core.util.Util.toString(dbgVariableNames)); //$NON-NLS-1$ } affectedProject .setRawLoadpath( affectedProject.getRawLoadpath(), SetLoadpathOperation.DO_NOT_SET_OUTPUT, null, // don't call beginTask on the monitor (see http://bugs.eclipse.org/bugs/show_bug.cgi?id=3717) canChangeResources, (ILoadpathEntry[]) affectedProjectClasspaths.get(affectedProject), false, // updating - no need for early validation false); // updating - no need to save } } }, null/*no need to lock anything*/, monitor); } catch (CoreException e) { if (CP_RESOLVE_VERBOSE){ Util.verbose( "CPVariable SET - FAILED DUE TO EXCEPTION\n" + //$NON-NLS-1$ " variables: " + org.rubypeople.rdt.core.util.Util.toString(dbgVariableNames), //$NON-NLS-1$ System.err); e.printStackTrace(); } if (e instanceof RubyModelException) { throw (RubyModelException)e; } else { throw new RubyModelException(e); } } } } private void variablePreferencesPut(String variableName, IPath[] variablePath) { String variableKey = CP_VARIABLE_PREFERENCES_PREFIX+variableName; if (variablePath == null) { this.variablesWithInitializer.remove(variableName); getInstancePreferences().remove(variableKey); } else { String string = ""; for (int i = 0; i < variablePath.length; i++) { if (i != 0) string += ";"; string += variablePath[i].toString(); } getInstancePreferences().put(variableKey, string); } try { getInstancePreferences().flush(); } catch (BackingStoreException e) { // ignore exception } } /** * Get workspace eclipse preference for RubyCore plugin. */ public IEclipsePreferences getInstancePreferences() { return preferencesLookup[PREF_INSTANCE]; } /* * Optimize startup case where 1 variable is initialized at a time with the same value as on shutdown. */ public boolean variablePutIfInitializingWithSameValue(String[] variableNames, IPath[][] variablePaths) { if (variableNames.length != 1) return false; String variableName = variableNames[0]; IPath[] oldPath = getPreviousSessionVariable(variableName); if (oldPath == null) return false; IPath[] newPath = variablePaths[0]; if (!oldPath.equals(newPath)) return false; variablePut(variableName, newPath); return true; } private final class VariablesAndContainersLoadHelper { private static final int ARRAY_INCREMENT = 200; private ILoadpathEntry[] allLoadpathEntries; private int allLoadpathEntryCount; private final Map<String, IPath> allPaths; private String[] allStrings; private int allStringsCount; private final DataInputStream in; VariablesAndContainersLoadHelper(DataInputStream in) { super(); this.allLoadpathEntries = null; this.allLoadpathEntryCount = 0; this.allPaths = new HashMap<String, IPath>(); this.allStrings = null; this.allStringsCount = 0; this.in = in; } void load() throws IOException { loadProjects(RubyModelManager.this.getRubyModel()); loadVariables(); } private boolean loadBoolean() throws IOException { return this.in.readBoolean(); } private ILoadpathEntry[] loadLoadpathEntries() throws IOException { int count = loadInt(); ILoadpathEntry[] entries = new ILoadpathEntry[count]; for (int i = 0; i < count; ++i) entries[i] = loadLoadpathEntry(); return entries; } private ILoadpathEntry loadLoadpathEntry() throws IOException { int id = loadInt(); if (id < 0 || id > this.allLoadpathEntryCount) throw new IOException("Unexpected loadpathentry id"); //$NON-NLS-1$ if (id < this.allLoadpathEntryCount) return this.allLoadpathEntries[id]; int entryKind = loadInt(); IPath path = loadPath(); IPath[] inclusionPatterns = loadPaths(); IPath[] exclusionPatterns = loadPaths(); boolean isExported = loadBoolean(); ILoadpathAttribute[] extraAttributes = loadAttributes(); ILoadpathEntry entry = new LoadpathEntry(entryKind, path, inclusionPatterns, exclusionPatterns, extraAttributes, isExported); ILoadpathEntry[] array = this.allLoadpathEntries; if (array == null || id == array.length) { array = new ILoadpathEntry[id + ARRAY_INCREMENT]; if (id != 0) System.arraycopy(this.allLoadpathEntries, 0, array, 0, id); this.allLoadpathEntries = array; } array[id] = entry; this.allLoadpathEntryCount = id + 1; return entry; } private ILoadpathAttribute[] loadAttributes() throws IOException { int count = loadInt(); if (count == 0) return LoadpathEntry.NO_EXTRA_ATTRIBUTES; ILoadpathAttribute[] attributes = new ILoadpathAttribute[count]; for (int i = 0; i < count; ++i) attributes[i] = loadAttribute(); return attributes; } private ILoadpathAttribute loadAttribute() throws IOException { String name = loadString(); String value = loadString(); return new LoadpathAttribute(name, value); } private void loadContainers(IRubyProject project) throws IOException { boolean projectIsAccessible = project.getProject().isAccessible(); int count = loadInt(); for (int i = 0; i < count; ++i) { IPath path = loadPath(); ILoadpathEntry[] entries = loadLoadpathEntries(); if (!projectIsAccessible) // avoid leaking deleted project's persisted container, // but still read the container as it is is part of the file format continue; ILoadpathContainer container = new PersistedLoadpathContainer(project, path, entries); RubyModelManager.this.containerPut(project, path, container); Map<IPath, ILoadpathContainer> oldContainers = RubyModelManager.this.previousSessionContainers.get(project); if (oldContainers == null) { oldContainers = new HashMap<IPath, ILoadpathContainer>(); RubyModelManager.this.previousSessionContainers.put(project, oldContainers); } oldContainers.put(path, container); } } private int loadInt() throws IOException { return this.in.readInt(); } private IPath loadPath() throws IOException { if (loadBoolean()) return null; String portableString = loadString(); IPath path = (IPath) this.allPaths.get(portableString); if (path == null) { path = Path.fromPortableString(portableString); this.allPaths.put(portableString, path); } return path; } private IPath[] loadPaths() throws IOException { int count = loadInt(); IPath[] pathArray = new IPath[count]; for (int i = 0; i < count; ++i) pathArray[i] = loadPath(); return pathArray; } private void loadProjects(IRubyModel model) throws IOException { int count = loadInt(); for (int i = 0; i < count; ++i) { String projectName = loadString(); loadContainers(model.getRubyProject(projectName)); } } private String loadString() throws IOException { int id = loadInt(); if (id < 0 || id > this.allStringsCount) throw new IOException("Unexpected string id"); //$NON-NLS-1$ if (id < this.allStringsCount) return this.allStrings[id]; String string = this.in.readUTF(); String[] array = this.allStrings; if (array == null || id == array.length) { array = new String[id + ARRAY_INCREMENT]; if (id != 0) System.arraycopy(this.allStrings, 0, array, 0, id); this.allStrings = array; } array[id] = string; this.allStringsCount = id + 1; return string; } private void loadVariables() throws IOException { int size = loadInt(); Map<String, IPath[]> loadedVars = new HashMap<String, IPath[]>(size); for (int i = 0; i < size; ++i) { String varName = loadString(); IPath[] varPath = loadPaths(); if (varPath != null) loadedVars.put(varName, varPath); } RubyModelManager.this.previousSessionVariables.putAll(loadedVars); RubyModelManager.this.variables.putAll(loadedVars); } } private static final class PersistedLoadpathContainer implements ILoadpathContainer { private final IPath containerPath; private final ILoadpathEntry[] entries; private final IRubyProject project; PersistedLoadpathContainer(IRubyProject project, IPath containerPath, ILoadpathEntry[] entries) { super(); this.containerPath = containerPath; this.entries = entries; this.project = project; } public ILoadpathEntry[] getLoadpathEntries() { return entries; } public String getDescription() { return "Persisted container [" + containerPath //$NON-NLS-1$ + " for project [" + project.getElementName() //$NON-NLS-1$ + "]]"; //$NON-NLS-1$ } public int getKind() { return 0; } public IPath getPath() { return containerPath; } public String toString() { return getDescription(); } } private final class VariablesAndContainersSaveHelper { private final HashtableOfObjectToInt loadpathEntryIds; // ILoadpathEntry -> int private final DataOutputStream out; private final HashtableOfObjectToInt stringIds; // Strings -> int VariablesAndContainersSaveHelper(DataOutputStream out) { super(); this.loadpathEntryIds = new HashtableOfObjectToInt(); this.out = out; this.stringIds = new HashtableOfObjectToInt(); } void save() throws IOException, RubyModelException { saveProjects(RubyModelManager.this.getRubyModel().getRubyProjects()); // remove variables that should not be saved HashMap<String, IPath[]> varsToSave = null; Iterator<Map.Entry<String, IPath[]>> iterator = RubyModelManager.this.variables.entrySet().iterator(); IEclipsePreferences defaultPreferences = getDefaultPreferences(); while (iterator.hasNext()) { Map.Entry<String, IPath[]> entry = iterator.next(); String varName = entry.getKey(); if (defaultPreferences.get(CP_VARIABLE_PREFERENCES_PREFIX + varName, null) != null // don't save classpath variables from the default preferences as there is no delta if they are removed || CP_ENTRY_IGNORE_PATH.equals(entry.getValue())) { if (varsToSave == null) varsToSave = new HashMap<String, IPath[]>(RubyModelManager.this.variables); varsToSave.remove(varName); } } saveVariables(varsToSave != null ? varsToSave : RubyModelManager.this.variables); } private void saveLoadpathEntries(ILoadpathEntry[] entries) throws IOException { int count = entries == null ? 0 : entries.length; saveInt(count); for (int i = 0; i < count; ++i) saveLoadpathEntry(entries[i]); } private void saveLoadpathEntry(ILoadpathEntry entry) throws IOException { if (saveNewId(entry, this.loadpathEntryIds)) { saveInt(entry.getEntryKind()); savePath(entry.getPath()); savePaths(entry.getInclusionPatterns()); savePaths(entry.getExclusionPatterns()); this.out.writeBoolean(entry.isExported()); saveAttributes(entry.getExtraAttributes()); } } private void saveAttribute(ILoadpathAttribute attribute) throws IOException { saveString(attribute.getName()); saveString(attribute.getValue()); } private void saveAttributes(ILoadpathAttribute[] attributes) throws IOException { int count = attributes == null ? 0 : attributes.length; saveInt(count); for (int i = 0; i < count; ++i) saveAttribute(attributes[i]); } private void saveContainers(IRubyProject project, Map<IPath, ILoadpathContainer> containerMap) throws IOException { saveInt(containerMap.size()); for (Iterator<Map.Entry<IPath, ILoadpathContainer>> i = containerMap.entrySet().iterator(); i.hasNext();) { Entry<IPath, ILoadpathContainer> entry = i.next(); IPath path = entry.getKey(); ILoadpathContainer container = entry.getValue(); ILoadpathEntry[] cpEntries = null; if (container == null) { // container has not been initialized yet, use previous // session value // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=73969) container = RubyModelManager.this.getPreviousSessionContainer(path, project); } if (container != null) cpEntries = container.getLoadpathEntries(); savePath(path); saveLoadpathEntries(cpEntries); } } private void saveInt(int value) throws IOException { this.out.writeInt(value); } private boolean saveNewId(Object key, HashtableOfObjectToInt map) throws IOException { int id = map.get(key); if (id == -1) { int newId = map.size(); map.put(key, newId); saveInt(newId); return true; } else { saveInt(id); return false; } } private void savePath(IPath path) throws IOException { if (path == null) { this.out.writeBoolean(true); } else { this.out.writeBoolean(false); saveString(path.toPortableString()); } } private void savePaths(IPath[] paths) throws IOException { int count = paths == null ? 0 : paths.length; saveInt(count); for (int i = 0; i < count; ++i) savePath(paths[i]); } private void saveProjects(IRubyProject[] projects) throws IOException, RubyModelException { int count = projects.length; saveInt(count); for (int i = 0; i < count; ++i) { IRubyProject project = projects[i]; saveString(project.getElementName()); Map<IPath, ILoadpathContainer> containerMap = RubyModelManager.this.containers.get(project); if (containerMap == null) { containerMap = Collections.emptyMap(); } else { // clone while iterating // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59638) containerMap = new HashMap<IPath, ILoadpathContainer>(containerMap); } saveContainers(project, containerMap); } } private void saveString(String string) throws IOException { if (saveNewId(string, this.stringIds)) this.out.writeUTF(string); } private void saveVariables(Map<String, IPath[]> map) throws IOException { saveInt(map.size()); for (String varName : map.keySet()) { IPath[] varPath = map.get(varName); saveString(varName); savePaths(varPath); } } } public IRubySearchScope getWorkspaceScope() { if (this.workspaceScope == null) { this.workspaceScope = new RubyWorkspaceScope(); } return this.workspaceScope; } public IndexManager getIndexManager() { return indexManager; } /** * Remove from secondary types cache all types belonging to a given file. * Clean secondary types cache built while indexing if requested. * * Project's secondary types cache is found using file location. * * @param file File to remove */ public void secondaryTypesRemoving(IFile file, boolean cleanIndexCache) { if (VERBOSE) { StringBuffer buffer = new StringBuffer("RubyModelManager.removeFromSecondaryTypesCache("); //$NON-NLS-1$ buffer.append(file.getName()); buffer.append(')'); Util.verbose(buffer.toString()); } if (file != null) { PerProjectInfo projectInfo = getPerProjectInfo(file.getProject(), false); if (projectInfo != null && projectInfo.secondaryTypes != null) { if (VERBOSE) { Util.verbose("-> remove file from cache of project: "+file.getProject().getName()); //$NON-NLS-1$ } // Clean current cache secondaryTypesRemoving(projectInfo.secondaryTypes, file); // Clean indexing cache if necessary if (!cleanIndexCache) return; HashMap<IFile, IType> indexingCache = (HashMap<IFile, IType>) projectInfo.secondaryTypes.get(INDEXED_SECONDARY_TYPES); if (indexingCache != null) { Set<IFile> keys = indexingCache.keySet(); int filesSize = keys.size(), filesCount = 0; IFile[] removed = null; Iterator<IFile> cachedFiles = keys.iterator(); while (cachedFiles.hasNext()) { IFile cachedFile = cachedFiles.next(); if (file.equals(cachedFile)) { if (removed == null) removed = new IFile[filesSize]; filesSize--; removed[filesCount++] = cachedFile; } } if (removed != null) { for (int i=0; i<filesCount; i++) { indexingCache.remove(removed[i]); } } } } } } /* * Remove from a given cache map all secondary types belonging to a given file. * Note that there can have several secondary types per file... */ private void secondaryTypesRemoving(Hashtable<String, HashMap<?, IType>> secondaryTypesMap, IFile file) { if (VERBOSE) { StringBuffer buffer = new StringBuffer("RubyModelManager.removeSecondaryTypesFromMap("); //$NON-NLS-1$ Iterator<String> keys = secondaryTypesMap.keySet().iterator(); while (keys.hasNext()) { String qualifiedName = keys.next(); buffer.append(qualifiedName+':'+secondaryTypesMap.get(qualifiedName)); } buffer.append(','); buffer.append(file.getFullPath()); buffer.append(')'); Util.verbose(buffer.toString()); } Set<String> packageKeys = secondaryTypesMap.keySet(); int packagesSize = packageKeys.size(), removedPackagesCount = 0; String[] removedPackages = null; Iterator<String> packages = packageKeys.iterator(); while (packages.hasNext()) { String packName = packages.next(); if (packName != INDEXED_SECONDARY_TYPES) { // skip indexing cache entry if present (!= is intentional) HashMap<String, IType> types = (HashMap<String, IType>) secondaryTypesMap.get(packName); Set<String> nameKeys = types.keySet(); int namesSize = nameKeys.size(), removedNamesCount = 0; String[] removedNames = null; Iterator<String> names = nameKeys.iterator(); while (names.hasNext()) { String typeName = names.next(); IType type = types.get(typeName); if (file.equals(type.getResource())) { if (removedNames == null) removedNames = new String[namesSize]; namesSize--; removedNames[removedNamesCount++] = typeName; } } if (removedNames != null) { for (int i=0; i<removedNamesCount; i++) { types.remove(removedNames[i]); } } if (types.size() == 0) { if (removedPackages == null) removedPackages = new String[packagesSize]; packagesSize--; removedPackages[removedPackagesCount++] = packName; } } } if (removedPackages != null) { for (int i=0; i<removedPackagesCount; i++) { secondaryTypesMap.remove(removedPackages[i]); } } if (VERBOSE) { Util.verbose(" - new secondary types map:"); //$NON-NLS-1$ Iterator<String> keys = secondaryTypesMap.keySet().iterator(); while (keys.hasNext()) { String qualifiedName = keys.next(); Util.verbose(" + "+qualifiedName+':'+secondaryTypesMap.get(qualifiedName) ); //$NON-NLS-1$ } } } private void traceVariableAndContainers(String action, long start) { Long delta = new Long(System.currentTimeMillis() - start); Long length = new Long(getVariableAndContainersFile().length()); String pattern = "{0} {1} bytes in variablesAndContainers.dat in {2}ms"; //$NON-NLS-1$ String message = MessageFormat.format(pattern, new Object[]{action, length, delta}); System.out.println(message); } public static boolean isVerbose() { return VERBOSE; } public synchronized String[] variableNames(){ int length = this.variables.size(); String[] result = new String[length]; Iterator<String> vars = this.variables.keySet().iterator(); int index = 0; while (vars.hasNext()) { result[index++] = vars.next(); } return result; } }