Search in Projects API

Today I got some help from Jaroslav Havlin, the creator of the new "Search in Projects API".

Below are the steps to create a search provider that finds recently modified files, via a new tab in the "Find in Projects" dialog:

Here's how to get to the above result.

  1. Create a new NetBeans module project named "RecentlyModifiedFilesSearch". Then set dependencies on these libraries:
    • Search in Projects API
    • Lookup API
    • Utilities API
    • Dialogs API
    • Datasystems API
    • File System API
    • Nodes API
  2. Create and register an implementation of "SearchProvider". This class tells the application the name of the provider and how it can be used. It should be registered via the @ServiceProvider annotation.

    Methods to implement:
    • Method createPresenter creates a new object that is added to the "Find in Projects" dialog when it is opened.
    • Method isReplaceSupported should return true if this provider support replacing, not only searching.
    • If you want to disable the search provider (e.g., there aren't required external tools available in the OS), return false from isEnabled.
    • Method getTitle returns a string that will be shown in the tab in the "Find in Projects" dialog. It can be localizable.

    Example file "org.netbeans.example.search.ExampleSearchProvider":
    package org.netbeans.example.search;
    
    import org.netbeans.spi.search.provider.SearchProvider;
    import org.netbeans.spi.search.provider.SearchProvider.Presenter;
    import org.openide.util.lookup.ServiceProvider;
    
    @ServiceProvider(service = SearchProvider.class)
    public class ExampleSearchProvider extends SearchProvider {
    
        @Override
        public Presenter createPresenter(boolean replaceMode) {
            return new ExampleSearchPresenter(this);
        }
    
        @Override
        public boolean isReplaceSupported() {
            return false;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    
        @Override
        public String getTitle() {
            return "Recent Files Search";
        }
    
    }   
  3. Next, we need to create a SearchProvider.Presenter. This is an object that is passed to the "Find in Projects" dialog and contains a visual component to show in the dialog, together with some methods to interact with it.

    Methods to implement:

    • Method getForm returns a JComponent that should contain controls for various search criteria. In the example below, we have controls for a file name pattern, search scope, and the age of files.
    • Method isUsable is called by the dialog to check whether the Find button should be enabled or not. You can use NotificationLineSupport passed as its argument to set a display error, warning, or info message.
    • Method composeSearch is used to apply the settings and prepare a search task. It returns a SearchComposition object, as shown below.
    • Please note that the example uses ComponentUtils.adjustComboForFileName (and similar methods), that modifies a JComboBox component to act as a combo box for selection of file name pattern. These methods were designed to make working with components created in a GUI Builder comfortable.
    • Remember to call fireChange whenever the value of any criteria changes.

    Example file "org.netbeans.example.search.ExampleSearchPresenter":
    package org.netbeans.example.search;
    
    import java.awt.FlowLayout;
    import javax.swing.BoxLayout;
    import javax.swing.JComboBox;
    import javax.swing.JComponent;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JSlider;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    import org.netbeans.api.search.SearchScopeOptions;
    import org.netbeans.api.search.ui.ComponentUtils;
    import org.netbeans.api.search.ui.FileNameController;
    import org.netbeans.api.search.ui.ScopeController;
    import org.netbeans.api.search.ui.ScopeOptionsController;
    import org.netbeans.spi.search.provider.SearchComposition;
    import org.netbeans.spi.search.provider.SearchProvider;
    import org.openide.NotificationLineSupport;
    import org.openide.util.HelpCtx;
    
    public class ExampleSearchPresenter extends SearchProvider.Presenter {
    
       private JPanel panel = null;
       ScopeOptionsController scopeSettingsPanel;
       FileNameController fileNameComboBox;
       ScopeController scopeComboBox;
       ChangeListener changeListener;
       JSlider slider;
    
       public ExampleSearchPresenter(SearchProvider searchProvider) {
           super(searchProvider, false);
       }
    
       /**
        * Get UI component that can be added to the search dialog.
        */
       @Override
       public synchronized JComponent getForm() {
           if (panel == null) {
               panel = new JPanel();
               panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
               JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEADING));
               JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEADING));
               JPanel row3 = new JPanel(new FlowLayout(FlowLayout.LEADING));
               row1.add(new JLabel("Age in hours: "));
               slider = new JSlider(1, 72);
               row1.add(slider);
               final JLabel hoursLabel = new JLabel(String.valueOf(slider.getValue()));
               row1.add(hoursLabel);
               row2.add(new JLabel("File name: "));
               fileNameComboBox = ComponentUtils.adjustComboForFileName(new JComboBox());
               row2.add(fileNameComboBox.getComponent());
               scopeSettingsPanel = ComponentUtils.adjustPanelForOptions(new JPanel(),
                       false, fileNameComboBox);
               row3.add(new JLabel("Scope: "));
               scopeComboBox = ComponentUtils.adjustComboForScope(new JComboBox(), null);
               row3.add(scopeComboBox.getComponent());
               panel.add(row1);
               panel.add(row3);
               panel.add(row2);
               panel.add(scopeSettingsPanel.getComponent());
               initChangeListener();
               slider.addChangeListener(new ChangeListener() {
                   @Override
                   public void stateChanged(ChangeEvent e) {
                       hoursLabel.setText(String.valueOf(slider.getValue()));
                   }
               });
           }
           return panel;
       }
    
       private void initChangeListener() {
           this.changeListener = new ChangeListener() {
               @Override
               public void stateChanged(ChangeEvent e) {
                   fireChange();
               }
           };
           fileNameComboBox.addChangeListener(changeListener);
           scopeSettingsPanel.addChangeListener(changeListener);
           slider.addChangeListener(changeListener);
       }
    
       @Override
       public HelpCtx getHelpCtx() {
           return null; // Some help should be provided, omitted for simplicity.
       }
    
       /**
        * Create search composition for criteria specified in the form.
        */
       @Override
       public SearchComposition<?> composeSearch() {
           SearchScopeOptions sso = scopeSettingsPanel.getSearchScopeOptions();
           return new ExampleSearchComposition(sso, scopeComboBox.getSearchInfo(),
                   slider.getValue(), this);
       }
    
       /**
        * Here we return always true, but could return false e.g. if file name
        * pattern is empty.
        */
       @Override
       public boolean isUsable(NotificationLineSupport notifySupport) {
           return true;
       }
    
    }   
  4. The last part of our search provider is the implementation of SearchComposition. This is a composition of various search parameters, the actual search algorithm, and the displayer that presents the results.

    Methods to implement:

    • The most important method here is start, which performs the actual search. In this case, SearchInfo and SearchScopeOptions objects are used for traversing. These objects were provided by controllers of GUI components (in the presenter). When something interesting is found, it should be displayed (with SearchResultsDisplayer.addMatchingObject).
    • Method getSearchResultsDisplayer should return the displayer associated with this composition. The displayer can be created by subclassing SearchResultsDisplayer class or simply by using the SearchResultsDisplayer.createDefault. Then you only need a helper object that can create nodes for found objects.

    Example file "org.netbeans.example.search.ExampleSearchComposition":
    package org.netbeans.example.search;
    
    public class ExampleSearchComposition extends SearchComposition<DataObject> {
    
       SearchScopeOptions searchScopeOptions;
       SearchInfo searchInfo;
       int oldInHours;
       SearchResultsDisplayer<DataObject> resultsDisplayer;
       private final Presenter presenter;
       AtomicBoolean terminated = new AtomicBoolean(false);
    
       public ExampleSearchComposition(SearchScopeOptions searchScopeOptions,
               SearchInfo searchInfo, int oldInHours, Presenter presenter) {
           this.searchScopeOptions = searchScopeOptions;
           this.searchInfo = searchInfo;
           this.oldInHours = oldInHours;
           this.presenter = presenter;
       }
    
       @Override
       public void start(SearchListener listener) {
           for (FileObject fo : searchInfo.getFilesToSearch(
                   searchScopeOptions, listener, terminated)) {
               if (ageInHours(fo) < oldInHours) {
                   try {
                       DataObject dob = DataObject.find(fo);
                       getSearchResultsDisplayer().addMatchingObject(dob);
                   } catch (DataObjectNotFoundException ex) {
                       listener.fileContentMatchingError(fo.getPath(), ex);
                   }
               }
           }
       }
    
       @Override
       public void terminate() {
           terminated.set(true);
       }
    
       @Override
       public boolean isTerminated() {
           return terminated.get();
       }
    
       /**
        * Use default displayer to show search results.
        */
       @Override
       public synchronized SearchResultsDisplayer<DataObject> getSearchResultsDisplayer() {
           if (resultsDisplayer == null) {
               resultsDisplayer = createResultsDisplayer();
           }
           return resultsDisplayer;
       }
    
       private SearchResultsDisplayer<DataObject> createResultsDisplayer() {
           /**
            * Object to transform matching objects to nodes.
            */
           SearchResultsDisplayer.NodeDisplayer<DataObject> nd =
                   new SearchResultsDisplayer.NodeDisplayer<DataObject>() {
                       @Override
                       public org.openide.nodes.Node matchToNode(
                               final DataObject match) {
                           return new FilterNode(match.getNodeDelegate()) {
                               @Override
                               public String getDisplayName() {
                                   return super.getDisplayName()
                                           + " (" + ageInMinutes(match.getPrimaryFile()) + " minutes old)";
                               }
                           };
                       }
                   };
           return SearchResultsDisplayer.createDefault(nd, this,
                   presenter, "less than " + oldInHours + " hours old");
       }
    
       private static long ageInMinutes(FileObject fo) {
           long fileDate = fo.lastModified().getTime();
           long now = System.currentTimeMillis();
           return (now - fileDate) / 60000;
       }
    
       private static long ageInHours(FileObject fo) {
           return ageInMinutes(fo) / 60;
       }
    
    }     

Run the module, select a node in the Projects window, press Ctrl-F, and you'll see the "Find in Projects" dialog has two tabs, the second is the one you provided above:

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today