| /* |
| * 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); |
| } |
| } |
| |
| } |