diff --git a/java.sourceui/src/org/netbeans/modules/java/source/ui/JavaTypeProvider.java b/java.sourceui/src/org/netbeans/modules/java/source/ui/JavaTypeProvider.java --- a/java.sourceui/src/org/netbeans/modules/java/source/ui/JavaTypeProvider.java +++ b/java.sourceui/src/org/netbeans/modules/java/source/ui/JavaTypeProvider.java @@ -29,6 +29,8 @@ * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. + * + * markiewb@netbeans.org * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding @@ -58,6 +60,8 @@ import org.netbeans.api.java.queries.SourceForBinaryQuery; import org.netbeans.api.java.source.ClassIndex; import org.netbeans.api.java.source.ClassIndex.NameKind; +import org.netbeans.api.java.source.ClassIndex.SearchScope; +import org.netbeans.api.java.source.ClassIndex.SearchScopeType; import org.netbeans.api.java.source.ClasspathInfo; import org.netbeans.api.java.source.ElementHandle; import org.netbeans.api.java.source.SourceUtils; @@ -87,7 +91,14 @@ import org.openide.util.NbBundle; /** + * Type provider which searches for declared class/enum types. It supports + * searching for fully qualified names - for example "java.util.Collections". As + * a side effect it supports matches of (sub-)packages based on the given + * package name - for example "java.C" searches in (sub-)packages with the + * prefix 'java' for types with the prefix 'C'. + * * @author Petr Hrebejk + * @author markiewb */ @org.openide.util.lookup.ServiceProvider(service=org.netbeans.spi.jumpto.type.TypeProvider.class) public class JavaTypeProvider implements TypeProvider { @@ -133,7 +144,7 @@ @Override public void computeTypeNames(Context context, final Result res) { isCanceled = false; - String text = context.getText(); + String originalText = context.getText(); SearchType searchType = context.getSearchType(); boolean hasBinaryOpen = Lookup.getDefault().lookup(BinaryElementOpen.class) != null; @@ -291,18 +302,13 @@ } else { res.setMessage(null); } + int lastIndexOfDot = originalText.lastIndexOf("."); + boolean isFullyQualifiedName = -1 != lastIndexOfDot; + final String packageName = (isFullyQualifiedName) ? originalText.substring(0, lastIndexOfDot) : ""; + final String typeName = (isFullyQualifiedName) ? originalText.substring(lastIndexOfDot + 1) : originalText; + final String textForQuery = getTextForQuery(typeName, nameKind, context.getSearchType()); - final String textForQuery; - switch( nameKind ) { - case REGEXP: - case CASE_INSENSITIVE_REGEXP: - text = removeNonJavaChars(text); - textForQuery = NameMatcherFactory.wildcardsToRegexp(text, searchType != SearchType.CASE_INSENSITIVE_EXACT_NAME); - break; - default: - textForQuery = text; - } - LOGGER.log(Level.FINE, "Text For Query ''{0}''.", text); + LOGGER.log(Level.FINE, "Text For Query ''{0}''.", originalText); if (customizer != null) { c = getCache(); if (c != null) { @@ -351,9 +357,9 @@ if (isCanceled) { return null; } - final Set> names = new HashSet> (ci.getDeclaredTypes(textForQuery,nameKind)); + final Set> names = new HashSet> (ci.getDeclaredTypes(packageName, textForQuery,nameKind)); if (nameKind == ClassIndex.NameKind.CAMEL_CASE) { - names.addAll(ci.getDeclaredTypes(textForQuery, ClassIndex.NameKind.CASE_INSENSITIVE_PREFIX)); + names.addAll(ci.getDeclaredTypes(packageName, textForQuery, ClassIndex.NameKind.CASE_INSENSITIVE_PREFIX)); } for (ElementHandle name : names) { JavaTypeDescription td = new JavaTypeDescription(ci, name); @@ -415,6 +421,19 @@ this.cache = cache; } + public static String getTextForQuery(String text, final NameKind nameKind, SearchType searchType) { + String textForQuery; + switch (nameKind) { + case REGEXP: + case CASE_INSENSITIVE_REGEXP: + textForQuery = NameMatcherFactory.wildcardsToRegexp(removeNonJavaChars(text), searchType != SearchType.CASE_INSENSITIVE_EXACT_NAME); + break; + default: + textForQuery = text; + } + return textForQuery; + } + //@NotTreadSafe static final class CacheItem { @@ -492,7 +511,7 @@ return cpInfo; } - public Set> getDeclaredTypes(final String name, final NameKind kind) { + public Set> getDeclaredTypes(final String packageName, final String typeName, final NameKind kind) { if (index == null) { final ClassPath cp = ClassPathSupport.createClassPath(root); index = isBinary ? @@ -501,7 +520,24 @@ JavaSourceAccessor.getINSTANCE().createClassIndex(ClassPath.EMPTY,cp,ClassPath.EMPTY,false): JavaSourceAccessor.getINSTANCE().createClassIndex(ClassPath.EMPTY,ClassPath.EMPTY,cp,false); } - return index.getDeclaredTypes(name,kind,EnumSet.of(isBinary?ClassIndex.SearchScope.DEPENDENCIES:ClassIndex.SearchScope.SOURCE)); + SearchScope baseSearchScope = isBinary ? ClassIndex.SearchScope.DEPENDENCIES : ClassIndex.SearchScope.SOURCE; + SearchScopeType searchScope; + // support FQN + if (!"".equals(packageName)) { + // has package in context (is fully qualified name), then limit the search to the package and its subpackages + Set subPackageNames = index.getPackageNames(packageName + ".", false, asSet(baseSearchScope)); + Set allPackages = new HashSet(); + allPackages.add(packageName); + allPackages.addAll(subPackageNames); + searchScope = ClassIndex.createPackageSearchScope(baseSearchScope, allPackages.toArray(new String[allPackages.size()])); + } else { + // else default behaviour + searchScope = baseSearchScope; + } + + + Set searchScopeSet = asSet(searchScope); + return index.getDeclaredTypes(typeName, kind, Collections.unmodifiableSet(searchScopeSet)); } private void initProjectInfo() { @@ -517,5 +553,8 @@ } } + private Set asSet(SearchScopeType searchScope) { + return new HashSet(Arrays.asList(searchScope)); + } } } diff --git a/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaTypeProviderTest.java b/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaTypeProviderTest.java new file mode 100644 --- /dev/null +++ b/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaTypeProviderTest.java @@ -0,0 +1,383 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package org.netbeans.modules.java.source.ui; + +import org.netbeans.api.java.source.*; +import com.sun.org.apache.bcel.internal.generic.AALOAD; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.URL; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.swing.event.ChangeListener; +import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.api.java.classpath.GlobalPathRegistry; +import org.netbeans.api.java.platform.JavaPlatformManager; +import org.netbeans.api.java.queries.SourceForBinaryQuery; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.source.parsing.FileObjects; +import org.netbeans.modules.java.source.ui.JavaTypeProvider; +import org.netbeans.modules.java.source.usages.ClassIndexManager; +import org.netbeans.modules.java.source.usages.ClassIndexManagerEvent; +import org.netbeans.modules.java.source.usages.ClassIndexManagerListener; +import org.netbeans.modules.java.source.usages.IndexUtil; +import org.netbeans.modules.parsing.api.indexing.IndexingManager; +import org.netbeans.modules.parsing.impl.indexing.RepositoryUpdater; +import org.netbeans.modules.parsing.lucene.support.IndexManager; +import org.netbeans.spi.java.classpath.ClassPathFactory; +import org.netbeans.spi.java.classpath.ClassPathImplementation; +import org.netbeans.spi.java.classpath.ClassPathProvider; +import org.netbeans.spi.java.classpath.PathResourceImplementation; +import org.netbeans.spi.java.classpath.support.ClassPathSupport; +import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation; +import org.netbeans.spi.jumpto.type.JumptoAccessor; +import org.netbeans.spi.jumpto.type.SearchType; +import org.netbeans.spi.jumpto.type.TypeDescriptor; +import org.netbeans.spi.jumpto.type.TypeProvider; +import org.openide.filesystems.FileLock; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileStateInvalidException; +import org.openide.filesystems.FileSystem; +import org.openide.filesystems.FileUtil; + +/** + * Test setup taken from org.netbeans.api.java.source.ClassIndexTest. + * + * @author Tomas Zezula + * @author markiewb + */ +public class JavaTypeProviderTest extends NbTestCase { + + private static FileObject srcRoot; + private static FileObject binRoot2; + private static FileObject libSrc2; + private static ClassPath sourcePath; + private static ClassPath compilePath; + private static ClassPath bootPath; + private static JavaTypeProviderTest.MutableCp spiCp; + private static JavaTypeProviderTest.MutableCp spiSrc; + private JavaTypeProvider provider; + + public JavaTypeProviderTest(String name) { + super(name); + } + String[] cA = {"ClassA", "org.me.pkg1"}; + String[] cB = {"ClassA", "org.me.pkg1sibling"}; + String[] cC = {"ClassA", "org.me.pkg1.subpackage"}; + String[] cD = {"ClassB", "org.me.pkg2"}; + String[] cE = {"ClassA", "com.other"}; + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + File cache = new File(getWorkDir(), "cache"); //NOI18N + cache.mkdirs(); + IndexUtil.setCacheFolder(cache); + File src = new File(getWorkDir(), "src"); //NOI18N + src.mkdirs(); + srcRoot = FileUtil.toFileObject(src); + srcRoot.createFolder("foo"); //NOI18N + src = new File(getWorkDir(), "src2"); //NOI18N + src.mkdirs(); + src = new File(getWorkDir(), "lib"); //NOI18N + src.mkdirs(); + src = new File(getWorkDir(), "lib2"); //NOI18N + src.mkdirs(); + binRoot2 = FileUtil.toFileObject(src); + src = new File(getWorkDir(), "lib2Src"); //NOI18N + src.mkdirs(); + libSrc2 = FileUtil.toFileObject(src); + spiSrc = new JavaTypeProviderTest.MutableCp(Collections.singletonList(ClassPathSupport.createResource(srcRoot.getURL()))); + sourcePath = ClassPathFactory.createClassPath(spiSrc); + spiCp = new JavaTypeProviderTest.MutableCp(); + compilePath = ClassPathFactory.createClassPath(spiCp); + bootPath = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries(); + MockServices.setServices(JavaTypeProviderTest.ClassPathProviderImpl.class, JavaTypeProviderTest.SFBQ.class); + + //create some types to be searched for later + String[][] testTypes = { + cA, cB, cC, cD, cE + }; + + for (String[] entry : testTypes) { + createJavaFile(srcRoot, entry[1], entry[0]); + } + IndexingManager.getDefault().refreshIndexAndWait(srcRoot.getURL(), null, true); + final ClassPath scp = ClassPathSupport.createClassPath(srcRoot); + final ClasspathInfo cpInfo = ClasspathInfo.create( + ClassPathSupport.createClassPath(new URL[0]), + ClassPathSupport.createClassPath(new URL[0]), + scp); + provider = new JavaTypeProvider(cpInfo, null); + + } + + @Override + protected void tearDown() throws Exception { + MockServices.setServices(); + } + + /** + * A full FQN should result in one class type. + * + * @throws Exception + */ + public void testGetDeclaredTypesScopes_FullyQualifiedName_Full() throws Exception { + //original behaviour - without packagename + { + String[][] expectedResults = {cE, cA, cC, cB}; + assertComputeTypeNames(expectedResults, "ClassA", provider); + } + //original behaviour - without packagename + { + String[][] expectedResults = {cD}; + assertComputeTypeNames(expectedResults, "ClassB", provider); + } + + } + + /** + * Tests the behaviour, when no package name is included in searchText. + * Packagenames should be ignored. + * + * @throws Exception + */ + public void testGetDeclaredTypesScopes_DefaultBehaviour() throws Exception { + //original behaviour - without packagename + { + String[][] expectedResults = {cE, cA, cC, cB}; + assertComputeTypeNames(expectedResults, "ClassA", provider); + } + //original behaviour - without packagename + { + String[][] expectedResults = {cD}; + assertComputeTypeNames(expectedResults, "ClassB", provider); + } + + } + + /** + * A partially given FQN may result in more than one class type. The + * packagename is used for retrival of package and subpackages, which + * results in more results. + * + * @throws Exception + */ + public void testGetDeclaredTypesScopes_FullyQualifiedName_Partial() throws Exception { + //search for classes with prefix "ClassA" and packages with (sub-)package "org.me.pkg1" + { + String[][] expectedResults = {cA, cC}; + assertComputeTypeNames(expectedResults, "org.me.pkg1.ClassA", provider); + } + //search for classes with prefix "ClassA" and packages with (sub-)package "org" + { + String[][] expectedResults = {cA, cC, cB}; + assertComputeTypeNames(expectedResults, "org.ClassA", provider); + } + //search for classes with prefix "ClassA" and packages with (sub-)package "org.me" + { + String[][] expectedResults = {cA, cC, cB}; + assertComputeTypeNames(expectedResults, "org.me.ClassA", provider); + } + //search for classes with prefix "Cl" and packages with (sub-)package "org.me" + { + String[][] expectedResults = {cA, cC, cB, cD}; + assertComputeTypeNames(expectedResults, "org.me.Cl", provider); + } + } + + private FileObject createJavaFile( + final FileObject root, + final String pkg, + final String name, + final String content) throws IOException { + final FileObject file = FileUtil.createData( + root, + String.format("%s/%s.java", + FileObjects.convertPackage2Folder(pkg), + name)); + final FileLock lck = file.lock(); + try { + final PrintWriter out = new PrintWriter(new OutputStreamWriter(file.getOutputStream(lck))); + try { + out.print(content); + } finally { + out.close(); + } + } finally { + lck.releaseLock(); + } + return file; + } + + private void assertComputeTypeNames(String[][] expectedResults, String searchText, TypeProvider provider) { + List results = new ArrayList(); + TypeProvider.Result res = JumptoAccessor.createResult(results); + TypeProvider.Context c = JumptoAccessor.createContext(null, searchText, SearchType.PREFIX); + + provider.computeTypeNames(c, res); + + assertEquals(expectedResults.length, results.size()); + //sort to make the result test run reproducable + Collections.sort(results, new Comparator() { + @Override + public int compare(TypeDescriptor o1, TypeDescriptor o2) { + int compareValue = o1.getSimpleName().compareToIgnoreCase(o2.getSimpleName()); + if (compareValue != 0) { + return compareValue; + } + return o1.getContextName().compareToIgnoreCase(o2.getContextName()); + } + }); + + for (int i = 0; i < results.size(); i++) { + + assertEquals("not equals at index " + i, expectedResults[i][0], results.get(i).getSimpleName()); + assertEquals("not equals at index " + i, " (" + expectedResults[i][1] + ")", results.get(i).getContextName()); + } + } + + private FileObject createJavaFile(FileObject srcRoot1, String packageName, String className) throws IOException { + return createJavaFile(srcRoot1, packageName, className, "package " + packageName + ";\npublic class " + className + " {}\n"); + } + + public static class ClassPathProviderImpl implements ClassPathProvider { + + public ClassPath findClassPath(final FileObject file, final String type) { + final FileObject[] roots = sourcePath.getRoots(); + for (FileObject root : roots) { + if (root.equals(file) || FileUtil.isParentOf(root, file)) { + if (type == ClassPath.SOURCE) { + return sourcePath; + } + if (type == ClassPath.COMPILE) { + return compilePath; + } + if (type == ClassPath.BOOT) { + return bootPath; + } + } + } + if (libSrc2.equals(file) || FileUtil.isParentOf(libSrc2, file)) { + if (type == ClassPath.SOURCE) { + return ClassPathSupport.createClassPath(new FileObject[]{libSrc2}); + } + if (type == ClassPath.COMPILE) { + return ClassPathSupport.createClassPath(new URL[0]); + } + if (type == ClassPath.BOOT) { + return bootPath; + } + } + return null; + } + } + + public static class SFBQ implements SourceForBinaryQueryImplementation { + + public SourceForBinaryQuery.Result findSourceRoots(URL binaryRoot) { + try { + if (binaryRoot.equals(binRoot2.getURL())) { + return new SourceForBinaryQuery.Result() { + public FileObject[] getRoots() { + return new FileObject[]{libSrc2}; + } + + public void addChangeListener(ChangeListener l) { + } + + public void removeChangeListener(ChangeListener l) { + } + }; + } + } catch (FileStateInvalidException e) { + } + return null; + } + } + + private static final class MutableCp implements ClassPathImplementation { + + private final PropertyChangeSupport support; + private List impls; + + public MutableCp() { + this(Collections.emptyList()); + } + + public MutableCp(final List impls) { + assert impls != null; + support = new PropertyChangeSupport(this); + this.impls = impls; + } + + public List getResources() { + return impls; + } + + public void addPropertyChangeListener(final PropertyChangeListener listener) { + assert listener != null; + this.support.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(final PropertyChangeListener listener) { + assert listener != null; + this.support.removePropertyChangeListener(listener); + } + + void setImpls(final List impls) { + assert impls != null; + this.impls = impls; + this.support.firePropertyChange(PROP_RESOURCES, null, null); + } + } +} diff --git a/java.sourceui/test/unit/src/org/netbeans/spi/jumpto/type/JumptoAccessor.java b/java.sourceui/test/unit/src/org/netbeans/spi/jumpto/type/JumptoAccessor.java --- a/java.sourceui/test/unit/src/org/netbeans/spi/jumpto/type/JumptoAccessor.java +++ b/java.sourceui/test/unit/src/org/netbeans/spi/jumpto/type/JumptoAccessor.java @@ -57,6 +57,7 @@ } public static Result createResult(List r) { - return new Result(r, new String[0]); + //may cause http://netbeans.org/bugzilla/show_bug.cgi?id=217180 + return new Result(r, new String[1]); } }