/** * Copyright (C) 2005 - 2014 Eric Van Dewoestine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.eclim.plugin.jdt.command.search; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.vfs.FileObject; import org.apache.commons.vfs.FileSystemManager; import org.apache.commons.vfs.VFS; import org.apache.tools.ant.taskdefs.condition.Os; import org.eclim.Services; import org.eclim.annotation.Command; import org.eclim.command.CommandLine; import org.eclim.command.Options; import org.eclim.plugin.core.command.AbstractCommand; import org.eclim.plugin.core.util.ProjectUtils; import org.eclim.plugin.jdt.util.JavaUtils; import org.eclim.util.StringUtils; import org.eclim.util.file.FileUtils; import org.eclim.util.file.Position; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.ICodeAssist; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; /** * Command to handle java search requests. * * @author Eric Van Dewoestine */ @Command( name = "java_search", options = "OPTIONAL n project ARG," + "OPTIONAL f file ARG," + "OPTIONAL o offset ARG," + "OPTIONAL e encoding ARG," + "OPTIONAL l length ARG," + "OPTIONAL p pattern ARG," + "OPTIONAL t type ARG," + "OPTIONAL x context ARG," + "OPTIONAL s scope ARG," + "OPTIONAL i case_insensitive NOARG" ) public class SearchCommand extends AbstractCommand { public static final String CONTEXT_ALL = "all"; public static final String CONTEXT_DECLARATIONS = "declarations"; public static final String CONTEXT_IMPLEMENTORS = "implementors"; public static final String CONTEXT_REFERENCES = "references"; public static final String SCOPE_ALL = "all"; public static final String SCOPE_PROJECT = "project"; public static final String SCOPE_TYPE = "type"; public static final String TYPE_ALL = "all"; public static final String TYPE_ANNOTATION = "annotation"; public static final String TYPE_CLASS = "class"; public static final String TYPE_CLASS_OR_ENUM = "classOrEnum"; public static final String TYPE_CLASS_OR_INTERFACE = "classOrInterface"; public static final String TYPE_CONSTRUCTOR = "constructor"; public static final String TYPE_ENUM = "enum"; public static final String TYPE_FIELD = "field"; public static final String TYPE_INTERFACE = "interface"; public static final String TYPE_METHOD = "method"; public static final String TYPE_PACKAGE = "package"; public static final String TYPE_TYPE = "type"; private static final Pattern INNER_CLASS = Pattern.compile("(.*?)(\\w+\\$)(\\w.*)"); protected static final String ANDROID_NATURE = "com.android.ide.eclipse.adt.AndroidNature"; private static final Pattern ANDROID_JDK_URL = Pattern.compile(".*android\\.jar!java.*"); /** * Key used for results that don't belong to any user specified sort key. */ private static final String DEFAULT_SORT_KEY = StringUtils.EMPTY; @Override public Object execute(CommandLine commandLine) throws Exception { List<SearchMatch> matches = executeSearch(commandLine); String projectName = commandLine.getValue(Options.NAME_OPTION); IProject project = projectName != null ? ProjectUtils.getProject(projectName) : null; String[] sortKeys = getSortKeys(project); // Store the results keyed by the sort key Map<String, List<Position>> positionMap = new HashMap<String, List<Position>>(); for(SearchMatch match : matches){ IJavaElement element = (IJavaElement)match.getElement(); if (element != null){ int elementType = element.getElementType(); if (elementType != IJavaElement.PACKAGE_FRAGMENT && elementType != IJavaElement.PACKAGE_FRAGMENT_ROOT) { Position result = createPosition(project, match); if (result != null) { String sortKey = getSortKey(result, sortKeys); List<Position> positions = positionMap.get(sortKey); if (positions == null) { positions = new ArrayList<Position>(); positionMap.put(sortKey, positions); } positions.add(result); } } } } // Assemble the final results in the sorted order List<Position> results = null; for (String sortKey : sortKeys) { List<Position> positions = positionMap.get(sortKey); if (positions == null) { continue; } if (results == null) { results = positions; } else { results.addAll(positions); } } return results == null ? Collections.emptyList() : results; } /** * Returns the sort keys for this project. It will include the default * key <code>DEFAULT_SORT_KEY</code>. * * @param project The current project. */ private String[] getSortKeys(IProject project) throws Exception { String[] sortSettings = getPreferences() .getArrayValue(project, "org.eclim.java.search.sort"); for(int i = 0; i < sortSettings.length; i++){ String sortKey = sortSettings[i]; sortKey = sortKey.replace('\\', '/'); if (!sortKey.endsWith("/")){ sortKey += '/'; } sortSettings[i] = sortKey; } String[] sortKeys = Arrays.copyOf(sortSettings, sortSettings.length + 1); sortKeys[sortKeys.length - 1] = DEFAULT_SORT_KEY; return sortKeys; } /** * Returns the sort key associated with the given <code>Position</code>. */ private String getSortKey(Position position, String[] sortKeys) throws Exception { String path = ProjectUtils.getProjectRelativePath(position.getFilename()); if (path != null){ for (String sortKey : sortKeys) { if (path.startsWith(sortKey)) { return sortKey; } } } return DEFAULT_SORT_KEY; } /** * Executes the search. * * @param commandLine The command line for the search. * @return The search results. */ public List<SearchMatch> executeSearch(CommandLine commandLine) throws Exception { int context = -1; if(commandLine.hasOption(Options.CONTEXT_OPTION)){ context = getContext(commandLine.getValue(Options.CONTEXT_OPTION)); } String project = commandLine.getValue(Options.NAME_OPTION); String scope = commandLine.getValue(Options.SCOPE_OPTION); String file = commandLine.getValue(Options.FILE_OPTION); String offset = commandLine.getValue(Options.OFFSET_OPTION); String length = commandLine.getValue(Options.LENGTH_OPTION); String pat = commandLine.getValue(Options.PATTERN_OPTION); SearchPattern pattern = null; IJavaProject javaProject = project != null ? JavaUtils.getJavaProject(project) : null; SearchRequestor requestor = new SearchRequestor(); // element search if(file != null && offset != null && length != null){ int charOffset = getOffset(commandLine); IJavaElement element = getElement( javaProject, file, charOffset, Integer.parseInt(length)); if(element != null){ // user requested a contextual search. if(context == -1){ context = getElementContextualContext(element); // jdt search doesn't support implementors for method searches, so // switch to declarations. }else if (context == IJavaSearchConstants.IMPLEMENTORS && element.getElementType() == IJavaElement.METHOD) { context = IJavaSearchConstants.DECLARATIONS; requestor = new ImplementorsSearchRequestor(); } pattern = SearchPattern.createPattern(element, context); } // pattern search }else if(pat != null){ if(context == -1){ context = IJavaSearchConstants.DECLARATIONS; } int matchType = SearchPattern.R_EXACT_MATCH; // wild card character supplied, use pattern matching. if(pat.indexOf('*') != -1 || pat.indexOf('?') != -1){ matchType = SearchPattern.R_PATTERN_MATCH; // all upper case, add camel case support. }else if(pat.equals(pat.toUpperCase())){ matchType |= SearchPattern.R_CAMELCASE_MATCH; } boolean caseSensitive = !commandLine.hasOption(Options.CASE_INSENSITIVE_OPTION); if(caseSensitive){ matchType |= SearchPattern.R_CASE_SENSITIVE; } int type = getType(commandLine.getValue(Options.TYPE_OPTION)); // jdt search doesn't support implementors for method searches, so switch // to declarations. if (type == IJavaSearchConstants.METHOD && context == IJavaSearchConstants.IMPLEMENTORS) { context = IJavaSearchConstants.DECLARATIONS; requestor = new ImplementorsSearchRequestor(); } // hack for inner classes Matcher matcher = INNER_CLASS.matcher(pat); if (matcher.matches()){ // pattern search doesn't support org.test.Type$Inner or // org.test.Type.Inner, so convert it to org.test.*Inner, then filter // the results. pattern = SearchPattern.createPattern( matcher.replaceFirst("$1*$3"), type, context, matchType); Pattern toMatch = Pattern.compile(pat .replace(".", "\\.") .replace("$", "\\$") .replace("(", "\\(") .replace(")", "\\)") .replace("*", ".*") .replace("?", ".")); List<SearchMatch> matches = search(pattern, getScope(scope, javaProject)); Iterator<SearchMatch> iterator = matches.iterator(); while (iterator.hasNext()){ SearchMatch match = iterator.next(); String name = JavaUtils.getFullyQualifiedName( (IJavaElement)match.getElement()).replace("#", "."); if (!toMatch.matcher(name).matches()){ iterator.remove(); } } return matches; } pattern = SearchPattern.createPattern(pat, type, context, matchType); // bad search request }else{ throw new IllegalArgumentException( Services.getMessage("java_search.indeterminate")); } List<SearchMatch> matches = search( pattern, getScope(scope, javaProject), requestor); return matches; } /** * Executes the search. * * @param pattern The search pattern. * @param scope The scope of the search (file, project, all, etc). * * @return List of matches. */ protected List<SearchMatch> search( SearchPattern pattern, IJavaSearchScope scope) throws CoreException { return search(pattern, scope, new SearchRequestor()); } /** * Executes the search. * * @param pattern The search pattern. * @param scope The scope of the search (file, project, all, etc). * @param requestor The search requestor used to accept matches. * * @return List of matches. */ protected List<SearchMatch> search( SearchPattern pattern, IJavaSearchScope scope, SearchRequestor requestor) throws CoreException { if(pattern != null){ SearchEngine engine = new SearchEngine(); SearchParticipant[] participants = new SearchParticipant[]{SearchEngine.getDefaultSearchParticipant()}; engine.search(pattern, participants, scope, requestor, null); } return requestor.getMatches(); } /** * Gets a IJavaElement by its position. * * @param javaProject The IJavaProject the file is in. * @param filename The file containing the element. * @param offset The offset of the element in the file. * @param length The lenght of the element. * @return The element. */ protected IJavaElement getElement( IJavaProject javaProject, String filename, int offset, int length) throws Exception { ICodeAssist code = null; try{ code = JavaUtils.getCompilationUnit(javaProject, filename); }catch(IllegalArgumentException iae){ // source not found, try location the class file. code = JavaUtils.findClassFile(javaProject, filename); } if (code != null){ IJavaElement[] elements = code.codeSelect(offset, length); if(elements != null && elements.length > 0){ return elements[0]; } } return null; } /** * Determines if the supplied path is a jar compatible path that can be * converted to a jar: url. * * @param path The IPath. * @return True if a jar or zip, false otherwise. */ protected boolean isJarArchive(IPath path) { String ext = path.getFileExtension(); return ext != null && ext.toLowerCase().matches("^(jar|zip)$"); } /** * Creates a Position from the supplied SearchMatch. * * @param project The project searching from. * @param match The SearchMatch. * @return The Position. */ protected Position createPosition(IProject project, SearchMatch match) throws Exception { IJavaElement element = (IJavaElement)match.getElement(); IJavaElement parent = JavaUtils.getPrimaryElement(element); String file = null; String elementName = JavaUtils.getFullyQualifiedName(parent); if(parent.getElementType() == IJavaElement.CLASS_FILE){ IResource resource = parent.getResource(); // occurs with a referenced project as a lib with no source and class // files that are not archived in that project if (resource != null && resource.getType() == IResource.FILE && !isJarArchive(resource.getLocation())) { file = resource.getLocation().toOSString(); }else{ IPath path = null; IPackageFragmentRoot root = (IPackageFragmentRoot) parent.getParent().getParent(); resource = root.getResource(); if (resource != null){ if (resource.getType() == IResource.PROJECT){ path = ProjectUtils.getIPath((IProject)resource); }else{ path = resource.getLocation(); } }else{ path = root.getPath(); } String classFile = elementName.replace('.', File.separatorChar); if (isJarArchive(path)){ file = "jar:file://" + path.toOSString() + '!' + classFile + ".class"; }else{ file = path.toOSString() + '/' + classFile + ".class"; } // android injects its jdk classes, so filter those out if the project // doesn't have the android nature. if (ANDROID_JDK_URL.matcher(file).matches() && project != null && !project.hasNature(ANDROID_NATURE)) { return null; } // if a source path attachment exists, use it. IPath srcPath = root.getSourceAttachmentPath(); if(srcPath != null){ String rootPath; IProject elementProject = root.getJavaProject().getProject(); // determine if src path is project relative or file system absolute. if(srcPath.isAbsolute() && elementProject.getName().equals(srcPath.segment(0))) { rootPath = ProjectUtils.getFilePath(elementProject, srcPath.toString()); }else{ rootPath = srcPath.toOSString(); } String srcFile = FileUtils.toUrl( rootPath + File.separator + classFile + ".java"); // see if source file exists at source path. FileSystemManager fsManager = VFS.getManager(); FileObject fileObject = fsManager.resolveFile(srcFile.replace("%", "%25")); if(fileObject.exists()){ file = srcFile; // jdk sources on osx are under a "src/" dir in the jar }else if (Os.isFamily(Os.FAMILY_MAC)){ srcFile = FileUtils.toUrl( rootPath + File.separator + "src" + File.separator + classFile + ".java"); fileObject = fsManager.resolveFile(srcFile.replace("%", "%25")); if(fileObject.exists()){ file = srcFile; } } } } }else{ IPath location = match.getResource().getLocation(); file = location != null ? location.toOSString() : null; } elementName = JavaUtils.getFullyQualifiedName(element); return Position.fromOffset( file.replace('\\', '/'), elementName, match.getOffset(), match.getLength()); } /** * Gets the search scope to use. * * @param scope The string name of the scope. * @param project The current project. * * @return The IJavaSearchScope equivalent. */ protected IJavaSearchScope getScope(String scope, IJavaProject project) throws Exception { if(project == null){ return SearchEngine.createWorkspaceScope(); }else if(SCOPE_PROJECT.equals(scope)){ return SearchEngine.createJavaSearchScope(new IJavaElement[]{project}); } return SearchEngine.createWorkspaceScope(); } /** * Determines the appropriate context to used base on the elements context. * * @param element The IJavaElement. * @return The int context */ protected int getElementContextualContext(IJavaElement element) { Class<?> theClass = element.getClass(); // type / field / method declaration if (theClass.equals(org.eclipse.jdt.internal.core.SourceType.class) || theClass.equals(org.eclipse.jdt.internal.core.SourceField.class) || theClass.equals(org.eclipse.jdt.internal.core.SourceMethod.class)) { return IJavaSearchConstants.REFERENCES; } return IJavaSearchConstants.DECLARATIONS; } /** * Translates the string context to the int equivalent. * * @param context The String context. * @return The int context */ protected int getContext(String context) { if(CONTEXT_ALL.equals(context)){ return IJavaSearchConstants.ALL_OCCURRENCES; }else if(CONTEXT_IMPLEMENTORS.equals(context)){ return IJavaSearchConstants.IMPLEMENTORS; }else if(CONTEXT_REFERENCES.equals(context)){ return IJavaSearchConstants.REFERENCES; } return IJavaSearchConstants.DECLARATIONS; } /** * Translates the string type to the int equivalent. * * @param type The String type. * @return The int type. */ protected int getType(String type) { if(TYPE_ANNOTATION.equals(type)){ return IJavaSearchConstants.ANNOTATION_TYPE; }else if(TYPE_CLASS.equals(type)){ return IJavaSearchConstants.CLASS; }else if(TYPE_CLASS_OR_ENUM.equals(type)){ return IJavaSearchConstants.CLASS_AND_ENUM; }else if(TYPE_CLASS_OR_INTERFACE.equals(type)){ return IJavaSearchConstants.CLASS_AND_INTERFACE; }else if(TYPE_CONSTRUCTOR.equals(type)){ return IJavaSearchConstants.CONSTRUCTOR; }else if(TYPE_ENUM.equals(type)){ return IJavaSearchConstants.ENUM; }else if(TYPE_FIELD.equals(type)){ return IJavaSearchConstants.FIELD; }else if(TYPE_INTERFACE.equals(type)){ return IJavaSearchConstants.INTERFACE; }else if(TYPE_METHOD.equals(type)){ return IJavaSearchConstants.METHOD; }else if(TYPE_PACKAGE.equals(type)){ return IJavaSearchConstants.PACKAGE; } return IJavaSearchConstants.TYPE; } }