blob: 2498cc503ba4dbc760a9d7e45417492ace248bee [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.java.source.ui;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.SimpleTypeVisitor6;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.ClassIndex;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.modules.java.source.usages.ClassIndexImpl;
import org.netbeans.modules.java.source.usages.ClassIndexManager;
import org.netbeans.modules.java.source.usages.DocumentUtil;
import org.netbeans.modules.parsing.lucene.support.IndexManager;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.spi.jumpto.support.NameMatcher;
import org.netbeans.spi.jumpto.support.NameMatcherFactory;
import org.netbeans.spi.jumpto.symbol.SymbolDescriptor;
import org.netbeans.spi.jumpto.symbol.SymbolProvider;
import org.netbeans.spi.jumpto.type.SearchType;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
/**
*
* @author Tomas Zezula
*/
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.spi.jumpto.symbol.SymbolProvider.class)
public class JavaSymbolProvider implements SymbolProvider {
private static final Logger LOGGER = Logger.getLogger(JavaSymbolProvider.class.getName());
private static final String CAPTURED_WILDCARD = "<captured wildcard>"; //NOI18N
private static final String UNKNOWN = "<unknown>"; //NOI18N
private volatile boolean canceled;
@Override
public String name() {
return "java symbols"; //NOI18N
}
@Override
public String getDisplayName() {
return NbBundle.getMessage(JavaTypeProvider.class, "MSG_JavaSymbols");
}
@Override
public void computeSymbolNames(final Context context, final Result result) {
try {
final SearchType st = context.getSearchType();
String textToSearch = context.getText();
final boolean scanInProgress = SourceUtils.isScanInProgress();
if (scanInProgress) {
// ui message
final String warningKind = NbBundle.getMessage(JavaSymbolProvider.class, "LBL_SymbolKind");
final String message = NbBundle.getMessage(JavaSymbolProvider.class, "LBL_ScanInProgress_warning", warningKind);
result.setMessage(message);
result.pendingResult();
final Cache cache = Cache.get(textToSearch, st);
if (cache != null) {
cache.populateResult(result);
return;
}
} else {
Cache.clear();
}
final Cache cache = scanInProgress ?
Cache.create(textToSearch, st) :
null;
String prefix = null;
final int dotIndex = textToSearch.lastIndexOf('.'); //NOI18N
if (dotIndex > 0 && dotIndex != textToSearch.length()-1) {
prefix = textToSearch.substring(0, dotIndex);
textToSearch = textToSearch.substring(dotIndex+1);
}
final String textToHighLight = textToSearch;
ClassIndex.NameKind _kind;
boolean _caseSensitive;
switch (st) {
case PREFIX:
_kind = ClassIndex.NameKind.PREFIX;
_caseSensitive = true;
break;
case REGEXP:
_kind = ClassIndex.NameKind.REGEXP;
textToSearch = NameMatcherFactory.wildcardsToRegexp(
removeNonJavaChars(textToSearch),
true);
_caseSensitive = true;
break;
case CAMEL_CASE:
_kind = ClassIndex.NameKind.CAMEL_CASE;
_caseSensitive = true;
break;
case CASE_INSENSITIVE_CAMEL_CASE:
_kind = ClassIndex.NameKind.CAMEL_CASE_INSENSITIVE;
_caseSensitive = false;
break;
case EXACT_NAME:
_kind = ClassIndex.NameKind.SIMPLE_NAME;
_caseSensitive = true;
break;
case CASE_INSENSITIVE_PREFIX:
_kind = ClassIndex.NameKind.CASE_INSENSITIVE_PREFIX;
_caseSensitive = false;
break;
case CASE_INSENSITIVE_EXACT_NAME:
_kind = ClassIndex.NameKind.CASE_INSENSITIVE_REGEXP;
_caseSensitive = false;
break;
case CASE_INSENSITIVE_REGEXP:
_kind = ClassIndex.NameKind.CASE_INSENSITIVE_REGEXP;
textToSearch = NameMatcherFactory.wildcardsToRegexp(
removeNonJavaChars(textToSearch),
true);
_caseSensitive = false;
break;
default:
throw new IllegalArgumentException();
}
final String ident = textToSearch;
final ClassIndex.NameKind kind = _kind;
final boolean caseSensitive = _caseSensitive;
final Pair<NameMatcher,Boolean> restriction;
if (prefix != null) {
restriction = compileName(prefix,caseSensitive);
result.setHighlightText(textToHighLight);
} else {
restriction = null;
}
try {
final ClassIndexManager manager = ClassIndexManager.getDefault();
Collection<FileObject> roots = QuerySupport.findRoots(
(Project)null,
Collections.singleton(ClassPath.SOURCE),
Collections.<String>emptySet(),
Collections.<String>emptySet());
final Set<URL> rootUrls = new HashSet<>();
for(FileObject root : roots) {
if (canceled) {
return;
}
rootUrls.add(root.toURL());
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Querying following roots:"); //NOI18N
for (URL url : rootUrls) {
LOGGER.log(Level.FINE, " {0}", url); //NOI18N
}
LOGGER.log(Level.FINE, "-------------------------"); //NOI18N
}
//Perform all queries in single op
IndexManager.priorityAccess(new IndexManager.Action<Void>() {
@Override
public Void run() throws IOException, InterruptedException {
for (URL url : rootUrls) {
if (canceled) {
return null;
}
final FileObject root = URLMapper.findFileObject(url);
if (root == null) {
continue;
}
final Project project = FileOwnerQuery.getOwner(root);
final ProjectInformation projectInfo = project == null ?
null :
project.getLookup().lookup(ProjectInformation.class); //Intentionally does not use ProjectUtils.getInformation() it does project icon annotation which is expensive
final ClassIndexImpl impl = manager.getUsagesQuery(root.toURL(), true);
if (impl != null) {
final Map<ElementHandle<TypeElement>,Set<String>> r = new HashMap<>();
impl.getDeclaredElements(ident, kind, DocumentUtil.typeElementConvertor(),r);
if (!r.isEmpty()) {
for (final Map.Entry<ElementHandle<TypeElement>,Set<String>> p : r.entrySet()) {
final ElementHandle<TypeElement> owner = p.getKey();
for (String symbol : p.getValue()) {
if (matchesRestrictions(owner.getQualifiedName(), symbol, restriction, caseSensitive)) {
final AsyncJavaSymbolDescriptor d = new AsyncJavaSymbolDescriptor(
projectInfo,
root,
impl,
owner,
symbol,
caseSensitive);
result.addResult(d);
if (cache != null) {
cache.offer(d);
}
}
}
}
}
}
}
return null;
}
});
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
catch (InterruptedException ie) {
return;
}
} finally {
clearCancel();
}
}
private static boolean matchesRestrictions(
@NonNull final String fqn,
@NonNull final String ident,
@NullAllowed Pair<NameMatcher,Boolean> restriction,
final boolean caseSensitive) {
return matchesRestrictionsImpl(fqn, ident, restriction, caseSensitive, false);
}
private static boolean matchesRestrictionsImpl(
@NonNull final String fqn,
@NonNull final String ident,
@NullAllowed Pair<NameMatcher,Boolean> restriction,
final boolean caseSensitive,
final boolean enclosing) {
if (restriction == null) {
return true;
}
final String simpleName = getSimpleName(fqn);
return restriction.first().accept(restriction.second() ? fqn : simpleName) ||
(!enclosing &&
(caseSensitive ? ident.equals(simpleName) : ident.equalsIgnoreCase(simpleName)) &&
matchesRestrictionsImpl(getOwner(fqn), ident, restriction, caseSensitive, true));
}
@NonNull
private static String getSimpleName(@NonNull final String fqn) {
final int index = fqn.lastIndexOf('.'); //NOI18N
return index < 0 ? fqn : fqn.substring(index+1);
}
@NonNull
private static String getOwner(@NonNull final String fqn) {
final int index = fqn.lastIndexOf('.'); //NOI18N
return index < 0 ? "" : fqn.substring(0, index); //NOI18N
}
private static Pair<NameMatcher,Boolean> compileName(
@NonNull final String prefix,
final boolean caseSensitive) {
final boolean fqn = prefix.indexOf('.') > 0; //NOI18N
final SearchType searchType = containsWildCard(prefix)?
(caseSensitive ? SearchType.REGEXP : SearchType.CASE_INSENSITIVE_REGEXP) :
(caseSensitive ? SearchType.PREFIX : SearchType.CASE_INSENSITIVE_PREFIX);
return Pair.<NameMatcher,Boolean>of(
NameMatcherFactory.createNameMatcher(prefix, searchType),
fqn);
}
private static boolean containsWildCard(String text) {
for( int i = 0; i < text.length(); i++ ) {
if ( text.charAt( i ) == '?' || text.charAt( i ) == '*' ) { // NOI18N
return true;
}
}
return false;
}
@NonNull
static Pair<String,String> getDisplayName (
@NonNull final Element e,
@NonNull final Element enclosingElement) {
assert e != null;
String name;
String suffix = null;
if (e.getKind() == ElementKind.METHOD || e.getKind() == ElementKind.CONSTRUCTOR) {
name = (e.getKind() == ElementKind.CONSTRUCTOR ?
enclosingElement.getSimpleName():
e.getSimpleName()).toString();
final StringBuilder sb = new StringBuilder();
sb.append('('); //NOI18N
ExecutableElement ee = (ExecutableElement) e;
final List<? extends VariableElement> vl = ee.getParameters();
for (Iterator<? extends VariableElement> it = vl.iterator(); it.hasNext();) {
final VariableElement v = it.next();
final TypeMirror tm = v.asType();
sb.append(getTypeName(tm, false, true));
if (it.hasNext()) {
sb.append(", "); //NOI18N
}
}
sb.append(')');
suffix = sb.toString();
} else {
name = e.getSimpleName().toString();
}
return Pair.of(name,suffix);
}
private static CharSequence getTypeName(TypeMirror type, boolean fqn, boolean varArg) {
if (type == null) {
return ""; //NOI18N
}
return new TypeNameVisitor(varArg).visit(type, fqn);
}
private static class TypeNameVisitor extends SimpleTypeVisitor6<StringBuilder,Boolean> {
private boolean varArg;
private boolean insideCapturedWildcard = false;
private TypeNameVisitor(boolean varArg) {
super(new StringBuilder());
this.varArg = varArg;
}
@Override
public StringBuilder defaultAction(TypeMirror t, Boolean p) {
return DEFAULT_VALUE.append(t);
}
@Override
public StringBuilder visitDeclared(DeclaredType t, Boolean p) {
Element e = t.asElement();
if (e instanceof TypeElement) {
TypeElement te = (TypeElement)e;
DEFAULT_VALUE.append((p ? te.getQualifiedName() : te.getSimpleName()).toString());
Iterator<? extends TypeMirror> it = t.getTypeArguments().iterator();
if (it.hasNext()) {
DEFAULT_VALUE.append("<"); //NOI18N
while(it.hasNext()) {
visit(it.next(), p);
if (it.hasNext()) {
DEFAULT_VALUE.append(", "); //NOI18N
}
}
DEFAULT_VALUE.append(">"); //NOI18N
}
return DEFAULT_VALUE;
} else {
return DEFAULT_VALUE.append(UNKNOWN); //NOI18N
}
}
@Override
public StringBuilder visitArray(ArrayType t, Boolean p) {
boolean isVarArg = varArg;
varArg = false;
visit(t.getComponentType(), p);
return DEFAULT_VALUE.append(isVarArg ? "..." : "[]"); //NOI18N
}
@Override
public StringBuilder visitTypeVariable(TypeVariable t, Boolean p) {
Element e = t.asElement();
if (e != null) {
String name = e.getSimpleName().toString();
if (!CAPTURED_WILDCARD.equals(name)) {
return DEFAULT_VALUE.append(name);
}
}
DEFAULT_VALUE.append("?"); //NOI18N
if (!insideCapturedWildcard) {
insideCapturedWildcard = true;
TypeMirror bound = t.getLowerBound();
if (bound != null && bound.getKind() != TypeKind.NULL) {
DEFAULT_VALUE.append(" super "); //NOI18N
visit(bound, p);
} else {
bound = t.getUpperBound();
if (bound != null && bound.getKind() != TypeKind.NULL) {
DEFAULT_VALUE.append(" extends "); //NOI18N
if (bound.getKind() == TypeKind.TYPEVAR) {
bound = ((TypeVariable)bound).getLowerBound();
}
visit(bound, p);
}
}
insideCapturedWildcard = false;
}
return DEFAULT_VALUE;
}
@Override
public StringBuilder visitWildcard(WildcardType t, Boolean p) {
int len = DEFAULT_VALUE.length();
DEFAULT_VALUE.append("?"); //NOI18N
TypeMirror bound = t.getSuperBound();
if (bound == null) {
bound = t.getExtendsBound();
if (bound != null) {
DEFAULT_VALUE.append(" extends "); //NOI18N
if (bound.getKind() == TypeKind.WILDCARD) {
bound = ((WildcardType)bound).getSuperBound();
}
visit(bound, p);
} else if (len == 0) {
bound = SourceUtils.getBound(t);
if (bound != null && (bound.getKind() != TypeKind.DECLARED || !((TypeElement)((DeclaredType)bound).asElement()).getQualifiedName().contentEquals("java.lang.Object"))) { //NOI18N
DEFAULT_VALUE.append(" extends "); //NOI18N
visit(bound, p);
}
}
} else {
DEFAULT_VALUE.append(" super "); //NOI18N
visit(bound, p);
}
return DEFAULT_VALUE;
}
@Override
public StringBuilder visitError(ErrorType t, Boolean p) {
Element e = t.asElement();
if (e instanceof TypeElement) {
TypeElement te = (TypeElement)e;
return DEFAULT_VALUE.append((p ? te.getQualifiedName() : te.getSimpleName()).toString());
}
return DEFAULT_VALUE;
}
}
private static String removeNonJavaChars(String text) {
StringBuilder sb = new StringBuilder();
for( int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if( Character.isJavaIdentifierPart(c) || c == '*' || c == '?') {
sb.append(c);
}
}
return sb.toString();
}
@Override
public void cancel() {
canceled = true;
}
@Override
public void cleanup() {
clearCancel();
Cache.clear();
}
private void clearCancel() {
canceled = false;
}
private static final class Cache {
private static Cache instance;
private final String text;
private final SearchType type;
private final Collection<SymbolDescriptor> descriptors;
private Cache(
@NonNull final String text,
@NonNull final SearchType type) {
this.text = text;
this.type = type;
this.descriptors = Collections.synchronizedSet(new HashSet<SymbolDescriptor>());
}
void populateResult(@NonNull final Result result) {
synchronized (descriptors) {
for (SymbolDescriptor d : descriptors) {
result.addResult(d);
}
}
}
void offer(@NonNull final AsyncJavaSymbolDescriptor d) {
descriptors.add(d);
}
static void clear() {
instance = null;
}
@CheckForNull
static Cache get(
@NonNull final String text,
@NonNull final SearchType type) {
Cache res = instance;
if (res != null && (!res.text.equals(text) || res.type != type)) {
res = instance = null;
}
return res;
}
@NonNull
static Cache create(
@NonNull final String text,
@NonNull final SearchType type) {
return instance = new Cache(text, type);
}
}
}