SearchService

The search service allows you to search workspaces:

  • For strings, numbers, and references to classes and/or members
  • With the ability to cancel the search early
  • With the ability to control which classes and files are searched in
  • With the ability to control what results are included in the final output

Query model

All searches are built from Query instances. There are three types of queries:

  • AndroidClassQuery (not yet implemented)
  • JvmClassQuery
  • FileQuery

Each implementation creates a SearchVisitor that handles searching of individual items in the Workspace. Things like string, number, and reference searches all implement each of these query types that they are relevant for. For example the reference search only implements AndroidClassQuery and JvmClassQuery but string search implements all three since strings can appear in any of these places (classes and files).

Searching for common cases like strings, numbers, and references are already implemented as queries and take in predicates for matching content. The following examples assume the following services are injected:

@Inject
NumberPredicateProvider numMatchProvider;
@Inject
StringPredicateProvider strMatchProvider;
@Inject
SearchService searchService;

String querying

Results results = searchService.search(classesWorkspace, new StringQuery(strMatchProvider.newEqualPredicate("Hello world")));
Results results = searchService.search(classesWorkspace, new StringQuery(strMatchProvider.newStartsWithPredicate("Hello")));
Results results = searchService.search(classesWorkspace, new StringQuery(strMatchProvider.newEndsWithPredicate("world")));

All the available built-in predicates come from StringPredicateProvider, or you can provide your own predicate implementation.

Number querying

Results results = searchService.search(classesWorkspace, new NumberQuery(numMatchProvider.newEqualsPredicate(4)));
Results results = searchService.search(classesWorkspace, new NumberQuery(numMatchProvider.newAnyOfPredicate(6, 32, 256)));
Results results = searchService.search(classesWorkspace, new NumberQuery(numMatchProvider.newRangePredicate(0, 10)));

All the available built-in predicates come from NumberPredicateProvider, or you can provide your own predicate implementation.

Reference querying

Each aspect of a reference (declaring class, name, descriptor) are their own string predicates. You pass null to any of these predicates to match anything for that given aspect. A simple example to find System.out.println() calls would look like:

Results results = searchService.search(classesWorkspace, new ReferenceQuery(
         strMatchProvider.newEqualPredicate("java/lang/System"),     // declaring class predicate
         strMatchProvider.newEqualPredicate("out"),                  // reference name predicate
         strMatchProvider.newEqualPredicate("Ljava/io/PrintStream;") // reference descriptor predicate
));

If you want to find all references to a given package you could do something like this:

Results results = searchService.search(classesWorkspace, new ReferenceQuery(
         strMatchProvider.newStartsWithPredicate("com/example/"),
         null, // match any field/method name
         null, // match any field/method descriptor
));

Feedback handler

Passing a feedback handler to the search(...) methods allows you to control what classes and files are searched in by implementing the doVisitClass(ClassInfo) and doVisitFile(FileInfo) methods. Here is a basic example which limits the search to only classes in a given package:

// All methods in the feedback interface default to visit everything, and include all results.
// You can override the 'boolean visitX' methods to control the searching of content within the passed classes/files.
class SkipClassesInPackage implements SearchFeedback {
    private final String pkg;
    
    SkipClassesInPackage(String pkg) { this.pkg = pkg; }
    
    @Override
    public boolean doVisitClass(@Nonnull ClassInfo cls) {
        // Skip if class does not exist in package
        return !cls.getName().startsWith(pkg);
    }
}
SearchFeedback skipping = new SkipClassesInPackage("com/example/");

To control the early abortion of a search you would implement hasRequestedCancellation() to return true after some point. A basic built in class can exists:

// There is a built-in cancellable search implementation.
CancellableSearchFeedback cancellable = new CancellableSearchFeedback();

// Aborts the current search that this feedback is associated with.
cancellable.cancel();

To limit which results are included in the final Results of the search(...) call, implementdoAcceptResult(Result<?>) to return false for results you want to discard. Since the Result contains a PathNode reference to where the match was made at, its probably what you'll want to operate on to implement your own filtering. Here is an example which limits the final Results to include only one item per class:

// This is a silly example, but we really just want to show off how you'd implement this, not how to make a real-world implementation.
class OnlyOneResultPerClass implements SearchFeedback {
    private Set<String> includedClassNames = new HashSet<>();
    
    @Override
    public boolean doAcceptResult(@Nonnull Result<?> result) {
        PathNode<?> pathToValue = result.getPath();
        
        // Get the class value in the path to the value.
        // If the path points to something more specific like a instruction in a method, then
        // this will be the class that defines te method with that instruction in it.
        ClassInfo classPathValue = pathToValue.getValueOfType(ClassInfo.class);
        if (classPathValue != null && !includedClassNames.add(classPathValue.getName())) {
            // If we've already seen a result from this class, skip all the remaining results
            // so that there is only one result per class.
            return false;
        }
        
        // Keep the result in the output
        return true;
    }
}