package org.rubypeople.rdt.internal.core.search; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.SubProgressMonitor; import org.rubypeople.rdt.core.Flags; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.IType; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.core.WorkingCopyOwner; import org.rubypeople.rdt.core.search.IRubySearchConstants; import org.rubypeople.rdt.core.search.IRubySearchScope; import org.rubypeople.rdt.core.search.SearchDocument; import org.rubypeople.rdt.core.search.SearchEngine; import org.rubypeople.rdt.core.search.SearchMatch; import org.rubypeople.rdt.core.search.SearchParticipant; import org.rubypeople.rdt.core.search.SearchPattern; import org.rubypeople.rdt.core.search.SearchRequestor; import org.rubypeople.rdt.core.search.TypeNameRequestor; import org.rubypeople.rdt.internal.core.DefaultWorkingCopyOwner; import org.rubypeople.rdt.internal.core.RubyModelManager; import org.rubypeople.rdt.internal.core.RubyProject; import org.rubypeople.rdt.internal.core.RubyScript; import org.rubypeople.rdt.internal.core.search.indexing.IIndexConstants; import org.rubypeople.rdt.internal.core.search.indexing.IndexManager; import org.rubypeople.rdt.internal.core.search.matching.MatchLocator; import org.rubypeople.rdt.internal.core.search.matching.RubySearchPattern; import org.rubypeople.rdt.internal.core.search.matching.TypeDeclarationPattern; import org.rubypeople.rdt.internal.core.util.CharOperation; import org.rubypeople.rdt.internal.core.util.Messages; import org.rubypeople.rdt.internal.core.util.Util; public class BasicSearchEngine { // Type decl kinds public static final int CLASS_DECL = 1; public static final int MODULE_DECL = 2; public static final boolean VERBOSE = false; /* * A list of working copies that take precedence over their original compilation units. */ private IRubyScript[] workingCopies; /* * A working copy owner whose working copies will take precedent over their original compilation units. */ private WorkingCopyOwner workingCopyOwner; /* * Creates a new search basic engine. */ public BasicSearchEngine() { // will use working copies of PRIMARY owner } /** * @see SearchEngine#SearchEngine(WorkingCopyOwner) for detailed comment. */ public BasicSearchEngine(WorkingCopyOwner workingCopyOwner) { this.workingCopyOwner = workingCopyOwner; } /** * Searches for matches of a given search pattern. Search patterns can be created using helper methods (from a * String pattern or a Ruby element) and encapsulate the description of what is being searched (for example, search * method declarations in a case sensitive way). * * @see SearchEngine#search(SearchPattern, SearchParticipant[], IRubySearchScope, SearchRequestor, IProgressMonitor) * for detailed comment */ public void search(SearchPattern pattern, SearchParticipant[] participants, IRubySearchScope scope, SearchRequestor requestor, IProgressMonitor monitor) throws CoreException { if (VERBOSE) { Util .verbose("BasicSearchEngine.search(SearchPattern, SearchParticipant[], IRubySearchScope, SearchRequestor, IProgressMonitor)"); //$NON-NLS-1$ } findMatches(pattern, participants, scope, requestor, monitor); } /** * @see SearchEngine#createRubySearchScope(IRubyElement[]) for detailed comment. */ public static IRubySearchScope createRubySearchScope(IRubyElement[] elements) { return createRubySearchScope(elements, true); } /** * @see SearchEngine#createRubySearchScope(IRubyElement[], boolean) for detailed comment. */ public static IRubySearchScope createRubySearchScope(IRubyElement[] elements, boolean includeReferencedProjects) { int includeMask = IRubySearchScope.SOURCES | IRubySearchScope.APPLICATION_LIBRARIES | IRubySearchScope.SYSTEM_LIBRARIES; if (includeReferencedProjects) { includeMask |= IRubySearchScope.REFERENCED_PROJECTS; } return createRubySearchScope(elements, includeMask); } /** * @see SearchEngine#createRubySearchScope(IRubyElement[], int) for detailed comment. */ public static IRubySearchScope createRubySearchScope(IRubyElement[] elements, int includeMask) { RubySearchScope scope = new RubySearchScope(); HashSet visitedProjects = new HashSet(2); for (int i = 0, length = elements.length; i < length; i++) { IRubyElement element = elements[i]; if (element != null) { try { if (element instanceof RubyProject) { scope.add((RubyProject) element, includeMask, visitedProjects); } else { scope.add(element); } } catch (RubyModelException e) { // ignore } } } return scope; } /** * Searches for matches to a given query. Search queries can be created using helper methods (from a String pattern * or a Ruby element) and encapsulate the description of what is being searched (for example, search method * declarations in a case sensitive way). * * @param scope * the search result has to be limited to the given scope * @param requestor * a callback object to which each match is reported */ void findMatches(SearchPattern pattern, SearchParticipant[] participants, IRubySearchScope scope, SearchRequestor requestor, IProgressMonitor monitor) throws CoreException { if (monitor != null && monitor.isCanceled()) throw new OperationCanceledException(); try { /* initialize progress monitor */ if (monitor != null) monitor.beginTask(Messages.engine_searching, 100); if (VERBOSE) { Util.verbose("Searching for pattern: " + pattern.toString()); //$NON-NLS-1$ Util.verbose(scope.toString()); } if (participants == null) { if (VERBOSE) Util.verbose("No participants => do nothing!"); //$NON-NLS-1$ return; } IndexManager indexManager = RubyModelManager.getRubyModelManager().getIndexManager(); requestor.beginReporting(); for (int i = 0, l = participants.length; i < l; i++) { if (monitor != null && monitor.isCanceled()) throw new OperationCanceledException(); SearchParticipant participant = participants[i]; SubProgressMonitor subMonitor = monitor == null ? null : new SubProgressMonitor(monitor, 1000); if (subMonitor != null) subMonitor.beginTask("", 1000); //$NON-NLS-1$ try { if (subMonitor != null) subMonitor.subTask(Messages.bind(Messages.engine_searching_indexing, new String[] { participant .getDescription() })); participant.beginSearching(); requestor.enterParticipant(participant); PathCollector pathCollector = new PathCollector(); indexManager.performConcurrentJob(new PatternSearchJob(pattern, participant, scope, pathCollector), IRubySearchConstants.WAIT_UNTIL_READY_TO_SEARCH, subMonitor); if (monitor != null && monitor.isCanceled()) throw new OperationCanceledException(); // locate index matches if any (note that all search matches could have been issued during index // querying) if (subMonitor != null) subMonitor.subTask(Messages.bind(Messages.engine_searching_matching, new String[] { participant .getDescription() })); String[] indexMatchPaths = pathCollector.getPaths(); if (indexMatchPaths != null) { pathCollector = null; // release int indexMatchLength = indexMatchPaths.length; SearchDocument[] indexMatches = new SearchDocument[indexMatchLength]; for (int j = 0; j < indexMatchLength; j++) { indexMatches[j] = participant.getDocument(indexMatchPaths[j]); } SearchDocument[] matches = MatchLocator.addWorkingCopies(pattern, indexMatches, getWorkingCopies(), participant); participant.locateMatches(matches, pattern, scope, requestor, subMonitor); } } finally { requestor.exitParticipant(participant); participant.doneSearching(); } } } finally { requestor.endReporting(); if (monitor != null) monitor.done(); } } /* * Returns the list of working copies used by this search engine. Returns null if none. */ private IRubyScript[] getWorkingCopies() { IRubyScript[] copies; if (this.workingCopies != null) { if (this.workingCopyOwner == null) { copies = RubyModelManager .getRubyModelManager() .getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, false/* don't add primary WCs a second time */); if (copies == null) { copies = this.workingCopies; } else { HashMap pathToCUs = new HashMap(); for (int i = 0, length = copies.length; i < length; i++) { IRubyScript unit = copies[i]; pathToCUs.put(unit.getPath(), unit); } for (int i = 0, length = this.workingCopies.length; i < length; i++) { IRubyScript unit = this.workingCopies[i]; pathToCUs.put(unit.getPath(), unit); } int length = pathToCUs.size(); copies = new IRubyScript[length]; pathToCUs.values().toArray(copies); } } else { copies = this.workingCopies; } } else if (this.workingCopyOwner != null) { copies = RubyModelManager.getRubyModelManager().getWorkingCopies(this.workingCopyOwner, true/* * add primary * WCs */); } else { copies = RubyModelManager.getRubyModelManager() .getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, false/* don't add primary WCs a second time */); } if (copies == null) return null; // filter out primary working copies that are saved IRubyScript[] result = null; int length = copies.length; int index = 0; for (int i = 0; i < length; i++) { RubyScript copy = (RubyScript) copies[i]; try { if (!copy.isPrimary() || copy.hasUnsavedChanges() || copy.hasResourceChanged()) { if (result == null) { result = new IRubyScript[length]; } result[index++] = copy; } } catch (RubyModelException e) { // copy doesn't exist: ignore } } if (index != length && result != null) { System.arraycopy(result, 0, result = new IRubyScript[index], 0, index); } return result; } public static SearchParticipant getDefaultSearchParticipant() { return new RubySearchParticipant(); } public static IRubySearchScope createWorkspaceScope() { return RubyModelManager.getRubyModelManager().getWorkspaceScope(); } public static Collection<IType> findType(String simpleTypeName) { SearchPattern pattern = SearchPattern.createPattern(IRubyElement.TYPE, "*" + simpleTypeName + "*", IRubySearchConstants.DECLARATIONS, SearchPattern.R_PATTERN_MATCH); SearchParticipant[] participants = new SearchParticipant[] { getDefaultSearchParticipant() }; IRubySearchScope scope = createWorkspaceScope(); TypeRequestor requestor = new TypeRequestor(); try { new BasicSearchEngine().search(pattern, participants, scope, requestor, null); } catch (CoreException e) { RubyCore.log(e); } List<IType> types = new ArrayList<IType>(); List<IType> matches = requestor.getTypes(); for (IType type : matches) { if (Util.getSimpleName(type.getElementName()).equals(simpleTypeName)) types.add(type); } return types; } private static class TypeRequestor extends SearchRequestor { private List<IType> types = new ArrayList<IType>(); @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { Object element = match.getElement(); types.add((IType) element); } public List<IType> getTypes() { return types; } } /** * Searches for all top-level types and member types in the given scope. The search can be selecting specific types * (given a package or a type name prefix and match modes). * * @see SearchEngine#searchAllTypeNames(char[], char[], int, int, IRubySearchScope, TypeNameRequestor, int, * IProgressMonitor) for detailed comment */ public void searchAllTypeNames(final char[] namespace, final char[] typeName, final int matchRule, int searchFor, IRubySearchScope scope, final TypeNameRequestor nameRequestor, int waitingPolicy, IProgressMonitor progressMonitor) throws RubyModelException { if (VERBOSE) { Util .verbose("BasicSearchEngine.searchAllTypeNames(char[], char[], int, int, IRubySearchScope, IRestrictedAccessTypeRequestor, int, IProgressMonitor)"); //$NON-NLS-1$ Util.verbose(" - namespace: " + (namespace == null ? "null" : new String(namespace))); //$NON-NLS-1$ //$NON-NLS-2$ Util.verbose(" - type name: " + (typeName == null ? "null" : new String(typeName))); //$NON-NLS-1$ //$NON-NLS-2$ Util.verbose(" - match rule: " + getMatchRuleString(matchRule)); //$NON-NLS-1$ Util.verbose(" - search for: " + searchFor); //$NON-NLS-1$ Util.verbose(" - scope: " + scope); //$NON-NLS-1$ } // Return on invalid combination of namespace and type names if (namespace == null || namespace.length == 0) { if (typeName != null && typeName.length == 0) { if (VERBOSE) { Util.verbose(" => return no result due to invalid empty values for package and type names!"); //$NON-NLS-1$ } return; } } IndexManager indexManager = RubyModelManager.getRubyModelManager().getIndexManager(); final char typeSuffix; switch (searchFor) { case IRubySearchConstants.CLASS: typeSuffix = IIndexConstants.CLASS_SUFFIX; break; case IRubySearchConstants.MODULE: typeSuffix = IIndexConstants.MODULE_SUFFIX; break; default: typeSuffix = IIndexConstants.TYPE_SUFFIX; break; } final TypeDeclarationPattern pattern = new TypeDeclarationPattern(null, // ignore "packages" getEnclosingTypeNames(namespace), typeName, typeSuffix, matchRule); // Get working copy path(s). Store in a single string in case of only one to optimize comparison in requestor final HashSet<String> workingCopyPaths = new HashSet<String>(); String workingCopyPath = null; IRubyScript[] copies = getWorkingCopies(); final int copiesLength = copies == null ? 0 : copies.length; if (copies != null) { if (copiesLength == 1) { workingCopyPath = copies[0].getPath().toString(); } else { for (int i = 0; i < copiesLength; i++) { IRubyScript workingCopy = copies[i]; workingCopyPaths.add(workingCopy.getPath().toString()); } } } final String singleWkcpPath = workingCopyPath; // Index requestor IndexQueryRequestor searchRequestor = new IndexQueryRequestor() { public boolean acceptIndexMatch(String documentPath, SearchPattern indexRecord, SearchParticipant participant) { // Filter unexpected types TypeDeclarationPattern record = (TypeDeclarationPattern) indexRecord; if (record.enclosingTypeNames == IIndexConstants.ONE_ZERO_CHAR) { return true; // filter out local and anonymous classes } switch (copiesLength) { case 0: break; case 1: if (singleWkcpPath.equals(documentPath)) { return true; // filter out *the* working copy } break; default: if (workingCopyPaths.contains(documentPath)) { return true; // filter out working copies } break; } // Accept document path if (match(record.typeSuffix, record.modifiers)) { nameRequestor.acceptType(record.typeSuffix == IIndexConstants.MODULE_SUFFIX, record.pkg, record.simpleName, record.enclosingTypeNames, documentPath); } return true; } }; try { if (progressMonitor != null) { progressMonitor.beginTask(Messages.engine_searching, 100); } // add type names from indexes indexManager.performConcurrentJob(new PatternSearchJob(pattern, getDefaultSearchParticipant(), // Ruby // search // only scope, searchRequestor), waitingPolicy, progressMonitor == null ? null : new SubProgressMonitor( progressMonitor, 100)); // add type names from working copies if (copies != null) { for (int i = 0; i < copiesLength; i++) { IRubyScript workingCopy = copies[i]; if (!scope.encloses(workingCopy)) continue; final String path = workingCopy.getPath().toString(); if (workingCopy.isConsistent()) { // TODO Clean this up and figure out what we use instead of package names... // IPackageDeclaration[] packageDeclarations = workingCopy.getPackageDeclarations(); // char[] packageDeclaration = packageDeclarations.length == 0 ? CharOperation.NO_CHAR : // packageDeclarations[0].getElementName().toCharArray(); char[] packageDeclaration = CharOperation.NO_CHAR; IType[] allTypes = workingCopy.getAllTypes(); for (int j = 0, allTypesLength = allTypes.length; j < allTypesLength; j++) { IType type = allTypes[j]; IRubyElement parent = type.getParent(); char[][] enclosingTypeNames; if (parent instanceof IType) { char[] parentQualifiedName = ((IType) parent).getTypeQualifiedName("::").toCharArray(); enclosingTypeNames = CharOperation.splitOn("::", parentQualifiedName); } else { enclosingTypeNames = CharOperation.NO_CHAR_CHAR; } char[] simpleName = type.getElementName().toCharArray(); int kind; if (type.isClass()) { kind = CLASS_DECL; } else /* if (type.isModule()) */{ kind = MODULE_DECL; } if (match(typeSuffix, namespace, typeName, matchRule, kind, squish(enclosingTypeNames), simpleName)) { nameRequestor.acceptType(type.isModule(), packageDeclaration, simpleName, enclosingTypeNames, path); } } } else { // TODO Parse and traverse AST, report all type declarations... } } } } finally { if (progressMonitor != null) { progressMonitor.done(); } } } private char[] squish(char[][] enclosingTypeNames) { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < enclosingTypeNames.length; i++) { if (i != 0) buffer.append("::"); buffer.append(enclosingTypeNames[i]); } return buffer.toString().toCharArray(); } private char[][] getEnclosingTypeNames(char[] packageName) { if (packageName == null || packageName.length == 0) return null; String raw = new String(packageName); String[] parts = Util.getTypeNameParts(raw); char[][] enclosing = new char[parts.length][]; for (int i = 0; i < parts.length; i++) { enclosing[i] = parts[i].toCharArray(); } return enclosing; } boolean match(char patternTypeSuffix, int modifiers) { switch (patternTypeSuffix) { case IIndexConstants.CLASS_SUFFIX: return (modifiers & (Flags.AccModule)) == 0; case IIndexConstants.TYPE_SUFFIX: return true; case IIndexConstants.MODULE_SUFFIX: return (modifiers & Flags.AccModule) != 0; } return true; } boolean match(char patternTypeSuffix, char[] patternPkg, char[] patternTypeName, int matchRule, int typeKind, char[] pkg, char[] typeName) { switch (patternTypeSuffix) { case IIndexConstants.CLASS_SUFFIX: if (typeKind != CLASS_DECL) return false; break; case IIndexConstants.TYPE_SUFFIX: if (typeKind != CLASS_DECL && typeKind != MODULE_DECL) return false; break; case IIndexConstants.MODULE_SUFFIX: if (typeKind != MODULE_DECL) return false; break; } if (patternPkg != null) { if (!doMatch(patternPkg, pkg, matchRule)) return false; } if (patternTypeName != null) { if (!doMatch(patternTypeName, typeName, matchRule)) return false; } return true; } private boolean isCaseSensitive(int matchRule) { return (matchRule & SearchPattern.R_CASE_SENSITIVE) != 0; } private boolean doMatch(char[] patternTypeName, char[] typeName, int matchRule) { boolean isCaseSensitive = isCaseSensitive(matchRule); boolean isCamelCase = (matchRule & SearchPattern.R_CAMELCASE_MATCH) != 0; int matchMode = matchRule & RubySearchPattern.MATCH_MODE_MASK; if (!isCaseSensitive && !isCamelCase) { patternTypeName = CharOperation.toLowerCase(patternTypeName); } boolean matchFirstChar = !isCaseSensitive || patternTypeName[0] == typeName[0]; if (isCamelCase && matchFirstChar && CharOperation.camelCaseMatch(patternTypeName, typeName)) { return true; } switch (matchMode) { case SearchPattern.R_EXACT_MATCH: if (!isCamelCase) { return matchFirstChar && CharOperation.equals(patternTypeName, typeName, isCaseSensitive); } // fall through next case to match as prefix if camel case failed case SearchPattern.R_PREFIX_MATCH: return matchFirstChar && CharOperation.prefixEquals(patternTypeName, typeName, isCaseSensitive); case SearchPattern.R_PATTERN_MATCH: return CharOperation.match(patternTypeName, typeName, isCaseSensitive); case SearchPattern.R_REGEXP_MATCH: // TODO (frederic) implement regular expression match return true; } return true; } /** * @param matchRule */ public static String getMatchRuleString(final int matchRule) { if (matchRule == 0) { return "R_EXACT_MATCH"; //$NON-NLS-1$ } StringBuffer buffer = new StringBuffer(); for (int i = 1; i <= 8; i++) { int bit = matchRule & (1 << (i - 1)); if (bit != 0 && buffer.length() > 0) buffer.append(" | "); //$NON-NLS-1$ switch (bit) { case SearchPattern.R_PREFIX_MATCH: buffer.append("R_PREFIX_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_CASE_SENSITIVE: buffer.append("R_CASE_SENSITIVE"); //$NON-NLS-1$ break; case SearchPattern.R_EQUIVALENT_MATCH: buffer.append("R_EQUIVALENT_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_ERASURE_MATCH: buffer.append("R_ERASURE_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_FULL_MATCH: buffer.append("R_FULL_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_PATTERN_MATCH: buffer.append("R_PATTERN_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_REGEXP_MATCH: buffer.append("R_REGEXP_MATCH"); //$NON-NLS-1$ break; case SearchPattern.R_CAMELCASE_MATCH: buffer.append("R_CAMELCASE_MATCH"); //$NON-NLS-1$ break; } } return buffer.toString(); } /** * @see SearchEngine#createHierarchyScope(IType) for detailed comment. */ public static IRubySearchScope createHierarchyScope(IType type) throws RubyModelException { return createHierarchyScope(type, DefaultWorkingCopyOwner.PRIMARY); } /** * @see SearchEngine#createHierarchyScope(IType,WorkingCopyOwner) for detailed comment. */ public static IRubySearchScope createHierarchyScope(IType type, WorkingCopyOwner owner) throws RubyModelException { return new HierarchyScope(type, owner); } }