package org.rubypeople.rdt.internal.core.search;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.rubypeople.rdt.core.IField;
import org.rubypeople.rdt.core.ILoadpathContainer;
import org.rubypeople.rdt.core.ILoadpathEntry;
import org.rubypeople.rdt.core.IMember;
import org.rubypeople.rdt.core.IRubyElement;
import org.rubypeople.rdt.core.IRubyElementDelta;
import org.rubypeople.rdt.core.IRubyModel;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.ISourceFolderRoot;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.RubyModelException;
import org.rubypeople.rdt.core.search.IRubySearchScope;
import org.rubypeople.rdt.internal.core.LoadpathEntry;
import org.rubypeople.rdt.internal.core.RubyModelManager;
import org.rubypeople.rdt.internal.core.RubyProject;
import org.rubypeople.rdt.internal.core.SourceFolder;
import org.rubypeople.rdt.internal.core.util.Util;
public class RubySearchScope extends AbstractSearchScope implements IRubySearchScope
{
private List<IRubyElement> elements;
/*
* The paths of the resources in this search scope (or the classpath entries' paths if the resources are projects)
*/
private ArrayList<String> projectPaths = new ArrayList<String>(); // container paths projects
private int[] projectIndexes; // Indexes of projects in list
private String[] containerPaths; // path to the container (e.g. /P/src, /P/lib.jar, c:\temp\mylib.jar)
private String[] relativePaths; // path relative to the container (e.g. x/y/Z.class, x/y, (empty))
private boolean[] isPkgPath; // in the case of packages, matches must be direct children of the folder
private int pathsCount;
private int threshold;
private IPath[] enclosingProjectsAndJars;
public RubySearchScope()
{
this(5);
}
private RubySearchScope(int size)
{
initialize(size);
}
protected void initialize(int size)
{
this.pathsCount = 0;
this.threshold = size; // size represents the expected number of elements
int extraRoom = (int) (size * 1.75f);
if (this.threshold == extraRoom)
extraRoom++;
this.relativePaths = new String[extraRoom];
this.containerPaths = new String[extraRoom];
this.projectPaths = new ArrayList<String>();
this.projectIndexes = new int[extraRoom];
this.isPkgPath = new boolean[extraRoom];
this.enclosingProjectsAndJars = new IPath[0];
}
/*
* E.g. 1. /P/src/pkg/X.java 2. /P/src/pkg 3. /P/lib.jar|org/eclipse/jdt/core/IJavaElement.class 4.
* /home/mylib.jar|x/y/z/X.class 5. c:\temp\mylib.jar|x/y/Y.class
* @see IJavaSearchScope#encloses(String)
*/
public boolean encloses(String resourcePathString)
{
// resource in workspace (case 1 or 2)
int index1 = indexOf(resourcePathString);
if (index1 >= 0)
return true;
// resource in external file
for (int i = 0; i < this.containerPaths.length; i++)
{
String containerPath = this.containerPaths[i];
if (containerPath == null)
continue;
if (resourcePathString.startsWith(containerPath))
return true;
}
return false;
}
/**
* Returns paths list index of given path or -1 if not found. NOTE: Use indexOf(String, String) for path inside jars
*
* @param fullPath
* the full path of the resource, e.g. 1. /P/src/pkg/X.java 2. /P/src/pkg
*/
private int indexOf(String fullPath)
{
// cannot guess the index of the container path
// fallback to sequentially looking at all known paths
for (int i = 0, length = this.relativePaths.length; i < length; i++)
{
String currentRelativePath = this.relativePaths[i];
if (currentRelativePath == null)
continue;
String currentContainerPath = this.containerPaths[i];
String currentFullPath = currentRelativePath.length() == 0 ? currentContainerPath
: (currentContainerPath + '/' + currentRelativePath);
if (encloses(currentFullPath, fullPath, i))
return i;
}
return -1;
}
/*
* Returns whether the enclosing path encloses the given path (or is equal to it)
*/
private boolean encloses(String enclosingPath, String path, int index)
{
// normalize given path as it can come from outside
path = normalize(path);
int pathLength = path.length();
int enclosingLength = enclosingPath.length();
if (pathLength < enclosingLength)
{
return false;
}
if (enclosingLength == 0)
{
return true;
}
if (pathLength == enclosingLength)
{
return path.equals(enclosingPath);
}
if (!this.isPkgPath[index])
{
return path.startsWith(enclosingPath) && path.charAt(enclosingLength) == '/';
}
else
{
// if looking at a package, this scope encloses the given path
// if the given path is a direct child of the folder
// or if the given path path is the folder path (see bug 13919 Declaration for package not found if scope is
// not project)
if (path.startsWith(enclosingPath)
&& ((enclosingPath.length() == path.lastIndexOf('/')) || (enclosingPath.length() == path.length())))
{
return true;
}
}
return false;
}
/**
* Adds the given path to this search scope. Remember if subfolders need to be included and associated access
* restriction as well.
*/
private void add(String projectPath, String relativePath, String containerPath, boolean isPackage)
{
// normalize containerPath and relativePath
containerPath = normalize(containerPath);
relativePath = normalize(relativePath);
int length = this.containerPaths.length, index = (containerPath.hashCode() & 0x7FFFFFFF) % length;
String currentRelativePath, currentContainerPath;
while ((currentRelativePath = this.relativePaths[index]) != null
&& (currentContainerPath = this.containerPaths[index]) != null)
{
if (currentRelativePath.equals(relativePath) && currentContainerPath.equals(containerPath))
return;
if (++index == length)
{
index = 0;
}
}
int idx = this.projectPaths.indexOf(projectPath);
if (idx == -1)
{
// store project in separated list to minimize memory footprint
this.projectPaths.add(projectPath);
idx = this.projectPaths.indexOf(projectPath);
}
this.projectIndexes[index] = idx;
this.relativePaths[index] = relativePath;
this.containerPaths[index] = containerPath;
this.isPkgPath[index] = isPackage;
// assumes the threshold is never equal to the size of the table
if (++this.pathsCount > this.threshold)
rehash();
}
/**
* Add a path to current java search scope or all project fragment roots if null. Use project resolved classpath to
* retrieve and store access restriction on each classpath entry. Recurse if dependent projects are found.
*
* @param rubyProject
* Project used to get resolved classpath entries
* @param pathToAdd
* Path to add in case of single element or null if user want to add all project package fragment roots
* @param includeMask
* Mask to apply on classpath entries
* @param visitedProjects
* Set to avoid infinite recursion
* @param referringEntry
* Project raw entry in referring project classpath
* @throws RubyModelException
* May happen while getting java model info
*/
void add(RubyProject rubyProject, IPath pathToAdd, int includeMask, HashSet visitedProjects,
ILoadpathEntry referringEntry) throws RubyModelException
{
IProject project = rubyProject.getProject();
if (!project.isAccessible() || !visitedProjects.add(project))
return;
IPath projectPath = project.getFullPath();
String projectPathString = projectPath.toString();
this.addEnclosingProjectOrJar(projectPath);
ILoadpathEntry[] entries = rubyProject.getResolvedLoadpath(true);
IRubyModel model = rubyProject.getRubyModel();
RubyModelManager.PerProjectInfo perProjectInfo = rubyProject.getPerProjectInfo();
for (int i = 0, length = entries.length; i < length; i++)
{
ILoadpathEntry entry = entries[i];
LoadpathEntry cpEntry = (LoadpathEntry) entry;
if (referringEntry != null)
{
// Add only exported entries.
// Source folder are implicitly exported.
if (!entry.isExported() && entry.getEntryKind() != ILoadpathEntry.CPE_SOURCE)
continue;
cpEntry = cpEntry.combineWith((LoadpathEntry) referringEntry);
// cpEntry = ((LoadpathEntry)referringEntry).combineWith(cpEntry);
}
switch (entry.getEntryKind())
{
case ILoadpathEntry.CPE_LIBRARY:
ILoadpathEntry rawEntry = null;
Map resolvedPathToRawEntries = perProjectInfo.resolvedPathToRawEntries;
if (resolvedPathToRawEntries != null)
{
rawEntry = (ILoadpathEntry) resolvedPathToRawEntries.get(entry.getPath());
}
if (rawEntry == null)
break;
switch (rawEntry.getEntryKind())
{
case ILoadpathEntry.CPE_LIBRARY:
case ILoadpathEntry.CPE_VARIABLE:
if ((includeMask & APPLICATION_LIBRARIES) != 0)
{
IPath path = entry.getPath();
if (pathToAdd == null || pathToAdd.equals(path))
{
String pathToString = path.getDevice() == null ? path.toString() : path
.toOSString();
add(projectPath.toString(), "", pathToString, false/* not a package */); //$NON-NLS-1$
addEnclosingProjectOrJar(path);
}
}
break;
case ILoadpathEntry.CPE_CONTAINER:
ILoadpathContainer container = RubyCore.getLoadpathContainer(rawEntry.getPath(),
rubyProject);
if (container == null)
break;
if ((container.getKind() == ILoadpathContainer.K_APPLICATION && (includeMask & APPLICATION_LIBRARIES) != 0)
|| (includeMask & SYSTEM_LIBRARIES) != 0)
{
IPath path = entry.getPath();
if (pathToAdd == null || pathToAdd.equals(path))
{
String pathToString = path.getDevice() == null ? path.toString() : path
.toOSString();
add(projectPath.toString(), "", pathToString, false/* not a package */); //$NON-NLS-1$
addEnclosingProjectOrJar(path);
}
}
break;
}
break;
case ILoadpathEntry.CPE_PROJECT:
if ((includeMask & REFERENCED_PROJECTS) != 0)
{
IPath path = entry.getPath();
if (pathToAdd == null || pathToAdd.equals(path))
{
add((RubyProject) model.getRubyProject(entry.getPath().lastSegment()), null, includeMask,
visitedProjects, cpEntry);
}
}
break;
case ILoadpathEntry.CPE_SOURCE:
if ((includeMask & SOURCES) != 0)
{
IPath path = entry.getPath();
if (pathToAdd == null || pathToAdd.equals(path))
{
add(projectPath.toString(), Util.relativePath(path, 1/* remove project segment */),
projectPathString, false/* not a package */);
}
}
break;
}
}
}
private void addEnclosingProjectOrJar(IPath path)
{
int length = this.enclosingProjectsAndJars.length;
for (int i = 0; i < length; i++)
{
if (this.enclosingProjectsAndJars[i].equals(path))
return;
}
System.arraycopy(this.enclosingProjectsAndJars, 0, this.enclosingProjectsAndJars = new IPath[length + 1], 0,
length);
this.enclosingProjectsAndJars[length] = path;
}
/*
* Removes trailing slashes from the given path
*/
private String normalize(String path)
{
int pathLength = path.length();
int index = pathLength - 1;
while (index >= 0 && path.charAt(index) == '/')
index--;
if (index != pathLength - 1)
return path.substring(0, index + 1);
return path;
}
private void rehash()
{
RubySearchScope newScope = new RubySearchScope(this.pathsCount * 2); // double the number of expected elements
newScope.projectPaths.ensureCapacity(this.projectPaths.size());
String currentPath;
for (int i = this.relativePaths.length; --i >= 0;)
if ((currentPath = this.relativePaths[i]) != null)
{
int idx = this.projectIndexes[i];
if (this.projectPaths.size() <= idx)
{
newScope.add(currentPath, this.containerPaths[i], this.isPkgPath[i]);
}
else
{
String projectPath = idx == -1 ? null : (String) this.projectPaths.get(idx);
newScope.add(projectPath, currentPath, this.containerPaths[i], this.isPkgPath[i]);
}
}
this.relativePaths = newScope.relativePaths;
this.containerPaths = newScope.containerPaths;
this.projectPaths = newScope.projectPaths;
this.projectIndexes = newScope.projectIndexes;
this.isPkgPath = newScope.isPkgPath;
this.threshold = newScope.threshold;
}
/*
* (non-Javadoc)
* @see IJavaSearchScope#enclosingProjectsAndJars()
*/
public IPath[] enclosingProjectsAndJars()
{
return this.enclosingProjectsAndJars;
}
/**
* Add ruby project all fragment roots to current ruby search scope.
*
* @see #add(RubyProject, IPath, int, HashSet, ILoadpathEntry)
*/
public void add(RubyProject project, int includeMask, HashSet visitedProject) throws RubyModelException
{
add(project, null, includeMask, visitedProject, null);
}
/**
* Add an element to the ruby search scope.
*
* @param element
* The element we want to add to current ruby search scope
* @throws RubyModelException
* May happen if some Ruby Model info are not available
*/
public void add(IRubyElement element) throws RubyModelException
{
IPath containerPath = null;
String containerPathToString = null;
int includeMask = SOURCES | APPLICATION_LIBRARIES | SYSTEM_LIBRARIES;
switch (element.getElementType())
{
case IRubyElement.RUBY_MODEL:
// a workspace sope should be used
break;
case IRubyElement.RUBY_PROJECT:
add((RubyProject) element, null, includeMask, new HashSet(2), null);
break;
case IRubyElement.SOURCE_FOLDER_ROOT:
ISourceFolderRoot root = (ISourceFolderRoot) element;
containerPath = root.getPath();
containerPathToString = containerPath.getDevice() == null ? containerPath.toString() : containerPath
.toOSString();
IResource rootResource = root.getResource();
if (rootResource != null && rootResource.isAccessible())
{
String relativePath = Util.relativePath(rootResource.getFullPath(), containerPath.segmentCount());
add(relativePath, containerPathToString, false/* not a package */);
}
else
{
add("", containerPathToString, false/* not a package */); //$NON-NLS-1$
}
break;
case IRubyElement.SOURCE_FOLDER:
root = (ISourceFolderRoot) element.getParent();
if (root.isExternal())
{
String relativePath = Util.concatWith(((SourceFolder) element).names, '/');
containerPath = root.getPath();
containerPathToString = containerPath.getDevice() == null ? containerPath.toString()
: containerPath.toOSString();
add(relativePath, containerPathToString, true/* package */);
}
else
{
IResource resource = element.getResource();
if (resource != null)
{
if (resource.isAccessible())
{
containerPath = root.getParent().getPath();
}
else
{
// for working copies, get resource container full path
containerPath = resource.getParent().getFullPath();
}
containerPathToString = containerPath.getDevice() == null ? containerPath.toString()
: containerPath.toOSString();
String relativePath = Util.relativePath(resource.getFullPath(), containerPath.segmentCount());
add(relativePath, containerPathToString, true/* package */);
}
}
break;
default:
// remember sub-script ruby elements
if (element instanceof IMember)
{
if (this.elements == null)
{
this.elements = new ArrayList<IRubyElement>();
}
this.elements.add(element);
}
root = (ISourceFolderRoot) element.getAncestor(IRubyElement.SOURCE_FOLDER_ROOT);
containerPath = root.getPath();
String relativePath = Util.relativePath(getPath(element, true/* full path */), 0/*
* remove project
* segment
*/);
containerPathToString = containerPath.getDevice() == null ? containerPath.toString() : containerPath
.toOSString();
add(relativePath, containerPathToString, false/* not a package */);
}
if (containerPath != null)
addEnclosingProjectOrJar(containerPath);
}
/**
* Adds the given path to this search scope. Remember if subfolders need to be included and associated access
* restriction as well.
*/
private void add(String relativePath, String containerPath, boolean isPackage)
{
// normalize containerPath and relativePath
containerPath = normalize(containerPath);
relativePath = normalize(relativePath);
int length = this.containerPaths.length, index = (containerPath.hashCode() & 0x7FFFFFFF) % length;
String currentRelativePath, currentContainerPath;
while ((currentRelativePath = this.relativePaths[index]) != null
&& (currentContainerPath = this.containerPaths[index]) != null)
{
if (currentRelativePath.equals(relativePath) && currentContainerPath.equals(containerPath))
return;
if (++index == length)
{
index = 0;
}
}
this.relativePaths[index] = relativePath;
this.containerPaths[index] = containerPath;
this.isPkgPath[index] = isPackage;
// assumes the threshold is never equal to the size of the table
if (++this.pathsCount > this.threshold)
rehash();
}
private IPath getPath(IRubyElement element, boolean relativeToRoot)
{
switch (element.getElementType())
{
case IRubyElement.RUBY_MODEL:
return Path.EMPTY;
case IRubyElement.RUBY_PROJECT:
return element.getPath();
case IRubyElement.SOURCE_FOLDER_ROOT:
if (relativeToRoot)
return Path.EMPTY;
return element.getPath();
case IRubyElement.SOURCE_FOLDER:
String relativePath = Util.concatWith(((SourceFolder) element).names, '/');
return getPath(element.getParent(), relativeToRoot).append(new Path(relativePath));
case IRubyElement.SCRIPT:
return getPath(element.getParent(), relativeToRoot).append(new Path(element.getElementName()));
default:
return getPath(element.getParent(), relativeToRoot);
}
}
/*
* (non-Javadoc)
* @see IRubySearchScope#encloses(IRubyElement)
*/
public boolean encloses(IRubyElement element)
{
if (this.elements != null)
{
for (int i = 0, length = this.elements.size(); i < length; i++)
{
IRubyElement scopeElement = this.elements.get(i);
// If we're searching for a field and scoped to something lower than a type, we need to expand scope
if (element instanceof IField)
{
if (element.isType(IRubyElement.GLOBAL)) // globals are scoped to file
{
scopeElement = scopeElement.getAncestor(IRubyElement.SCRIPT);
}
else // instance and class vars get scoped to type
{
scopeElement = scopeElement.getAncestor(IRubyElement.TYPE);
}
}
IRubyElement searchedElement = element;
while (searchedElement != null)
{
if (searchedElement.equals(scopeElement))
return true;
searchedElement = searchedElement.getParent();
}
}
return false;
}
ISourceFolderRoot root = (ISourceFolderRoot) element.getAncestor(IRubyElement.SOURCE_FOLDER_ROOT);
if (root != null && root.isExternal())
{
// external
IPath rootPath = root.getPath();
String rootPathToString = rootPath.getDevice() == null ? rootPath.toString() : rootPath.toOSString();
IPath relativePath = getPath(element, true/* relative path */);
return indexOf(rootPathToString, relativePath.toString()) >= 0;
}
// resource in workspace
String fullResourcePathString = getPath(element, false/* full path */).toString();
return indexOf(fullResourcePathString) >= 0;
}
/**
* Returns paths list index of given path or -1 if not found.
*
* @param containerPath
* the path of the container, e.g. 1. /P/src 2. /P 3. /P/lib.jar 4. /home/mylib.jar 5. c:\temp\mylib.jar
* @param relativePath
* the forward slash path relatively to the container, e.g. 1. x/y/Z.class 2. x/y 3. X.java 4. (empty)
*/
private int indexOf(String containerPath, String relativePath)
{
// use the hash to get faster comparison
int length = this.containerPaths.length, index = (containerPath.hashCode() & 0x7FFFFFFF) % length;
String currentContainerPath;
while ((currentContainerPath = this.containerPaths[index]) != null)
{
if (currentContainerPath.equals(containerPath))
{
String currentRelativePath = this.relativePaths[index];
if (encloses(currentRelativePath, relativePath, index))
return index;
}
if (++index == length)
{
index = 0;
}
}
return -1;
}
/*
* @see AbstractSearchScope#processDelta(IRubyElementDelta)
*/
public void processDelta(IRubyElementDelta delta, int eventType) {
switch (delta.getKind()) {
case IRubyElementDelta.CHANGED:
IRubyElementDelta[] children = delta.getAffectedChildren();
for (int i = 0, length = children.length; i < length; i++) {
IRubyElementDelta child = children[i];
this.processDelta(child, eventType);
}
break;
case IRubyElementDelta.REMOVED:
IRubyElement element = delta.getElement();
if (this.encloses(element)) {
if (this.elements != null) {
this.elements.remove(element);
}
IPath path = null;
switch (element.getElementType()) {
case IRubyElement.RUBY_PROJECT:
path = ((IRubyProject)element).getProject().getFullPath();
case IRubyElement.SOURCE_FOLDER_ROOT:
if (path == null) {
path = ((ISourceFolderRoot)element).getPath();
}
int toRemove = -1;
for (int i = 0; i < this.pathsCount; i++) {
if (this.relativePaths[i].equals(path)) { // TODO (jerome) this compares String and IPath !
toRemove = i;
break;
}
}
if (toRemove != -1) {
this.relativePaths[toRemove] = null;
rehash();
}
}
}
break;
}
}
}