TodoRCP2

By John Kostaras


This article is an update of TodoRCP which has also been published in Java Magazine. This update:

  • uses latest NetBeans 8.0.2 and JDK 8 and applies Java 8 features such as lambdas and the Stream API
  • provides some fixes to the original code
  • introduces a quick filter to filter the tasks outline view
  • introduces a filter button that allows to filter tasks by priorities

A 3rd update of this article will show how to migrate the ToDo PIM appplication to JavaFX 8.

Contents

Introduction

The source code for the RCP ToDo PIM application developed in the previous article can be downloaded here. The resulted application is shown in Figure 1:

"Figure 1 TodoRCP Application"

It's architecture consists of 4 modules as shown in Figure 2:

"Figure 2 TodoRCP Module Suite"

"Figure 2 TodoRCP Module Dependencies"

After you loaded the application in NetBeans, right-click on TodoRCP module suite, select Properties --> Libraries and select JDK 1.8 as the Java platform. The right-click on each module, select Properties --> Sources and set the source level to 1.8. Clean and build the project. This article will be based on the article published in Java Magazine.

Bug Fixes

In Listing 35, we converted TaskManager to observable:

private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);

The problem with this declaration is that it allows the this reference to leak (escape) in the constructor when it is not yet properly constructed, creating an object that is not thread safe. The safe way is as follows:

private PropertyChangeSupport pcs = null;

...
/**
  * @return a thread-safe PropertyChangeSupport
  */
private PropertyChangeSupport getPropertyChangeSupport() {
   if (pcs == null) {
       pcs = new PropertyChangeSupport(this);
   }
   return pcs;
}

and use the above method instead of the variable pcs. Same applies for Task in Listing 36.

Another bug is that we don't validate the input of the spinners of TaskDetailsDialog that we copied from the Swing todo app. Open the TaskDetailsDialog in design mode, select the priority JSpinner, and on the Properties window select the Events group. Locate the stateChanged property and click on the combo box to create a priorityStateChanged() method which will consume the ChangeEvent which is created when the JSpinner's value changes. Repeat for the daysBefore JSpinner.

The code that you need to complete is shown below. Mainly whenever the user enters an invalid value (i.e. out of bounds) the value is set to the respective bound. To do that, you need to stop listening to change events, do the validation, and then start listening again, because each time you call the setValue() method, a new ChangeEvent is generated.

private void priorityStateChanged(javax.swing.event.ChangeEvent evt) {                                      
    priority.removeChangeListener(formListener);
    int value = (int) priority.getValue();
    if (value < 0) priority.setValue(0);
    if (value > 100) priority.setValue(100);
    priority.addChangeListener(formListener);
}                                     

private void daysBeforeStateChanged(javax.swing.event.ChangeEvent evt) {                                       
    daysBefore.removeChangeListener(formListener);
    int value = (int) daysBefore.getValue();
    if (value < 0) daysBefore.setValue(0);
    if (value > 100) daysBefore.setValue(100);   // arbitrary upper bound
    daysBefore.addChangeListener(formListener);
}

However, if you try the above yourself, you will see that formListener is not defined (it is as local variable inside initComponents()) and you cannot change it from the design view. From an outside editor, hack TaskDetailsDialog by defining an attribute private FormListener formListener; and initialise it inside initComponents() like so:

formListener = new FormListener();

Execute the application and verify that priority and daysBefore spinners cannot take a value < 0 or > 100.

Apply Java 8 lambda expressions and streams

We can reduce boiler-plate code by using the new Java 8 functional features, simply by applying the hints that NetBeans provides.

In Listing 17, for loops can be replaced by the streams (method actionPerformed()):

public final class MarkAsCompletedTaskAction implements ActionListener {

    private final List<Task> context;

    public MarkAsCompletedTaskAction(List<Task> context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        context.stream().forEach((task) -> {
            //            task.setCompleted(true);
            taskManager.markAsCompleted(task.getId(), true);
        });
    }
}

In Listing 19, too, (method listTasksWithAlert()):

@Override
public List<Task> listTasksWithAlert() throws ModelException {
   final List<Task> tasksWithAlert = new ArrayList<>(tasks.size());
   tasks.stream().filter((task) -> (task.hasAlert())).forEach((task) -> {
      tasksWithAlert.add(task);
   });
   return Collections.unmodifiableList(tasksWithAlert);
}

TaskDetailsDialog's main() method can be written as:

public static void main(String args[]) {
   java.awt.EventQueue.invokeLater(() -> {
       new TaskDetailsDialog(new javax.swing.JFrame(), true).setVisible(true);
   });
}

In Listing 33, QuickFilter is a functional interface, so it can be written as:

private final QuickFilter filter = (Object aValue) -> {
    if (aValue instanceof TaskNode) {
       TaskNode taskNode = (TaskNode) aValue;
       Task task = taskNode.getLookup().lookup(Task.class);
       return task.isCompleted();
    }
    return true;
};

Finally, property change listeners can be transformed to lambda expressions. In Listing 36 class TaskChildFactory:

private final transient PropertyChangeListener pcl = (final PropertyChangeEvent evt) -> { refresh(true); };

In Listing 38 class TaskNode:

private final transient PropertyChangeListener pcl = (final PropertyChangeEvent evt) -> { firePropertySetsChange(null, getPropertySets()); };

Migrate to Java 8 java.time

Get rid of java.util.Date and java.util.Calendar classes. The new java.time is here. First stop is Task class; replace all instances of java.util.Date to java.time.LocalDate:

...
import java.time.LocalDate;

public class Task implements Serializable { 
  ...
  private LocalDate dueDate = LocalDate.now();
  ...
  public Task(int id, String descr, int prio, LocalDate due) {
     this(id, descr, prio, due, false, 0, "");
  }

  public Task(int id, String descr, int prio, LocalDate due, boolean alert, int daysBefore) {
     this(id, descr, prio, due, alert, daysBefore, "");
  }

  public Task(int id, String descr, int prio, LocalDate due, boolean alert, int daysBefore, String obs) {
  ...
  }

  // Get rid of
  // private Calendar getTodayDate() { ... }

  public boolean isLate() {
      LocalDate dueDate = getDueDate();
      if (dueDate == null) {
          return false;
      } else {
          return dueDate.compareTo(LocalDate.now()) < 0;
      }
  }

  public boolean hasAlert() {
      LocalDate dueDate = getDueDate();
      if (!getAlert() || dueDate == null) {
          return false;
      } else {
          int dias = dueDate.getDayOfYear() - LocalDate.now().getDayOfYear();
          return dias <= getDaysBefore();
      }
    }  

    public LocalDate getDueDate() {
        return dueDate;
    }

    public void setDueDate(LocalDate dueDate) {
        LocalDate oldValue = this.dueDate;
        this.dueDate = dueDate;
        getPropertyChangeSupport().firePropertyChange("DUE DATE", oldValue, dueDate);
    }
...
}

Same applies for the non-persistent TaskManager in Listing 35:

...
public class TaskManager implements TaskManagerInterface {
...
  public TaskManager() {
    tasks.add(new Task(1, "Hotel Reservation", 1, LocalDate.of(2014, 7, 2), true, 2));
    tasks.add(new Task(2, "Review BOF-1", 1, LocalDate.of(2014, 7, 6), true, 1));
    tasks.add(new Task(3, "Reserve time for visit", 2, LocalDate.of(2014, 7, 5)));
  }
...

or in Listing 39 if you use the persistent version:

...
public class TaskManager implements TaskManagerInterface {
...
  private List<Task> query(String where, String orderBy) throws DatabaseException {
  ...
    task.setDueDate(rs.getDate(5).toLocalDate());
  ...
}

  private void modify(String sql, Task task) throws DatabaseException {
  ...
    pst.setDate(4, Date.valueOf(task.getDueDate()));
  ...
}
...

TaskDetailsDialog complains:

  
  public void setTask(Task task) {
  ...
     pst.setDate(4, Date.valueOf(task.getDueDate()));
  ...
  }

  public Task getTask() {
  ...
task.setDueDate(dueDate.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
  ...
  }

And the most tricky part, the DatePropertyEditor:

...
@PropertyEditorRegistration(targetType = LocalDate.class)
public class DatePropertyEditor extends PropertyEditorSupport implements ExPropertyEditor, InplaceEditor.Factory {
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yy");
    private InplaceEditor ed;

    @Override
    public String getAsText() {
        LocalDate d = (LocalDate) getValue();
        if (d == null) {
            return "No Date Set";
        }
        return d.format(DATE_FORMATTER);
    }

    @Override
    public void setAsText(String s) {
        setValue(LocalDate.parse(s, DATE_FORMATTER));
    }
    ...
    private static class Inplace implements InplaceEditor {
    ...
        @Override
        public Object getValue() {
            return picker.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        }

        @Override
        public void setValue(Object object) {
            picker.setDate(Date.from(((LocalDate) object).atStartOfDay(ZoneId.systemDefault()).toInstant()));
        }
    ...
        @Override
        public void reset() {
            LocalDate d = (LocalDate) editor.getValue();
            if (d != null) {
                picker.setDate(Date.from(d.atStartOfDay(ZoneId.systemDefault()).toInstant()));
            }
        }
    ... 

Since picker uses java.util.Date we need to transform java.time.LocalDate used by Task to (setValue()) and from (getValue()) java.util.Date to be used by picker.

Introduce a quick filter

NetBeans, as of version 7.4, provides a new top component to display notifications! This window (see followng figure) provides an outline view to display notifications by category error/info/warning (each notification has priority, message, date created and category) as well as a details area on the right. It also contains a toolbar which displays two buttons:

  1. a quick filter button which hides/shows the quick filter text box on the bottom
  2. a filter button which displays the Notifications Filter dialog box (see Figure 4)

It would be nice if the RCP API allowed us to use and customise this window to our applications, like it does for other components of the netbeans platform.

"Figure 3 Notifications top component"

"Figure 4 Notifications Filter dialog box"

However, till version 8.0.2 this is not possible, to my knowledge at least. In this section we will see how to add a Quick Filter to our ToDo application. To achieve that, we will see how to copy and customize the NetBeans RCP code to create our own Filter functionality.

Note: There is a quick search on the toolbar (see Figure 5) already available. With this Quick Search Provider you can quickly select actions.

"Figure 5 Running on MacOSX"

To add this functionality to our application, there is only the hard way:

  1. Download the NetBeans sources and unzip them to a directory of your choice
  2. Copy netbeans/platform/modules/org-netbeans-modules-notifications.jar to a new location and unzip it.
  3. To avoid problems with missing module suites or platforms, create a new Module Project in NetBeans (i.e. in Figure 1 of the original article select Module instead of NetBeans Platform Application); name it e.g.FilterMenu and set the code base name as e.g. org.netbeans.filters.
  4. Copy the src and test directories from the unzipped org-netbeans-modules-notifications.jar to the FilterMenu project.
  5. Fix any dependencies.

Now you are ready to start customising the component in order to use in your project. I only kept the classes I need (see following figure).

"Figure 6 FilterMenu module"

These are the steps I did. This resulted in a new module which you can download here and use in your own projects.

I modified CategoryFilter to remove addDefaultTypes and I added a new constructor to initialise enabledCategories field which is now of generic type String:

public CategoryFilter(Set<String> categories) {
      enabledCategories.addAll(categories);
}

CategoriesPanel was renamed correctly and modified accordingly:

/**
  *
  * @param categories categories
  * @param filter filter to initialise panel with.
  */
public CategoriesPanel(List<String> categories, CategoryFilter filter) {
  this.filter = filter;
  this.categories = categories;
  init();
  setOpaque("Metal".equals(UIManager.getLookAndFeel().getID())); //NOI18N
}

I got rid of references to NotificationCenterManager and NotificationDisplayer.

private final List<Category> categories; was transformed to private final List<String> categories;

NotificationFilter was renamed to Filter and the first constructor was modified as follows:

/**
  *
  * @param name filter name
  * @param categories categories
  */
Filter(String name, Set<String> categories) {
  this.name = name;
  this.defaultCategoryFilter = new CategoryFilter(categories);
}

/**
  * @return a read-only set of enabled categories, or null if the filter is empty
  */
public Set<String> getActiveCategories() {
  return getCategoryFilter() == null ? 
         null : getCategoryFilter().getEnabledCategories();
}

FilterRepository was made observable and three more methods were added:

public void setQuickSearchFilter(String searchText) {
   String oldFilter = quickSearchFilter;
   quickSearchFilter = (searchText == null || searchText.isEmpty()) ? null : searchText;
   pcs.firePropertyChange(PROP_QUICK_FILTER, oldFilter, quickSearchFilter);
}

public boolean isQuickSearchFilter() {
   return quickSearchFilter != null;
}

public String getQuickSearchFilter() {
   return quickSearchFilter;
}

From the rest, NotificationSettings has been renamed to Settings and I have only kept FiltersMenuButton and MenuToggleButton.

In FiltersMenuButton I removed the dependency to NotificationCenterManager.

Finally, I opened up the packages to be module available (right click on the module FileMenu | Properties | API Versioning and selected org.netbeans.filters and org.netbeans.filters.filter packages to make publicly available. Clean and build FilterMenu module.

Integrate FilterMenu to TodoRCP

To integrate FilterMenu into TodoRCP, right-click on Modules of TodoRCP and select Add Existing...; navigate to FilterMenu and add it to TodoRCP module suite. If there is no bug with NetBeans, you should see it in its list of modules.

Let's start by creating the action in Controller module. Right-click on todo.controller.options package and select New --> Action. It will be an Always Enabled action under Options and will be called QuickFilterAction. Use the find16.png icon from org.netbeans.filters.resources. Then transform it to a BooleanStateAction as we saw in the previous article.

@ActionID(
        category = "Options",
        id = "todo.controller.options.QuickFilterAction"
)
@ActionRegistration(
        iconBase = "todo/controller/options/find16.png",
        displayName = "#CTL_QuickFilterAction"
)
@ActionReferences({
    @ActionReference(path = "Menu/Options", position = 50),
    @ActionReference(path = "Toolbars/Options", position = 50),
    @ActionReference(path = "Shortcuts", name = "F3")
})
@Messages("CTL_QuickFilterAction=Quick Filter")
public final class QuickFilterAction extends BooleanStateAction {

    @Override
    protected void initialize() {
        super.initialize();
        setBooleanState(Settings.isSearchVisible());
    }    
    
    @Override
    public void actionPerformed(ActionEvent e) {
        super.actionPerformed(e);
        TasksTopComponent tasksTopComponent = (TasksTopComponent) WindowManager.getDefault().findTopComponent("TasksTopComponent");
        tasksTopComponent.setSearchVisible(getBooleanState());
    }
    
    @Override
    public String getName() {
        return "Quick Filter";
    }

    @Override
    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

    @Override
    protected String iconResource() {
        return "todo/controller/options/find16.png";
    }    
}

To remove the first error, (Settings class), add a dependency to FilterMenu module. It will also complain about setSearchVisible() but this is what we are going to fix next.

The only class that needs to be modified is TasksTopComponent of View module. First we need to create a new JPanel which will contain the quick filter text box. Open TasksTopComponent in Design view, create a new panel (pnlQuickSearchFilter) and add it to the south of the top component. Define two new attributes:

    /** Quick Search filter */
    private final QuickSearch quickSearch;
    /** Callback */
    private final QuickSearch.Callback quickFilterCallback = new QuickFilterCallback();

and initialise them in the constructor:

        GridBagConstraints searchConstrains = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
        quickSearch = QuickSearch.attach(pnlQuickSearchFilter, searchConstrains, quickFilterCallback, true);
        FilterRepository.getInstance().addPropertyChangeListener(pcl);
        setSearchVisible(Settings.isSearchVisible());

Add a dependency to FilterMenu here, too. As you see, QuickSearch requires a panel, constrains, a filter callback (which we define next) and a boolean for asynchronous processing if true. Every time FilterRepository.getInstance().setQuickSearchFilter(searchText); is called, an event is raised.

/** Listen to changes in the FilterRepository and filter the outline view
  * accordingly. */
private final PropertyChangeListener pcl = (final PropertyChangeEvent evt) -> {
   SwingUtilities.invokeLater(() -> {
      // filter outline view on QuickSearchFilter input
      if (FilterRepository.PROP_QUICK_FILTER.equals(evt.getPropertyName())) {
         final String newQuickSearchFilter = (String) evt.getNewValue();
         if (newQuickSearchFilter != null && !newQuickSearchFilter.isEmpty()) {
            unsetQuickFilter();   // should run in EDT
            if (!newQuickSearchFilter.isEmpty()) { // should run in EDT
               setQuickFilter(0, getQuickFilter(newQuickSearchFilter));
            }
         } else {
               unsetQuickFilter();  // should run in EDT
         }
      }
   });
};

/**
  * Create a QuickFilter from the given String.
  *
  * @param qsFilter filter as String
  * @return a QuickFilter from the given String
  */
private QuickFilter getQuickFilter(String qsFilter) {
    return (Object aValue) -> {
       if (aValue instanceof TaskNode) {
            Task task = ((TaskNode) aValue).getLookup().lookup(Task.class);
            return task.getDescription().contains(qsFilter)
                   || String.valueOf(task.getPriority()).contains(qsFilter)
                   || task.getDueDate().toString().contains(qsFilter);
       } else {
            return false;
       }
   };
}
private class QuickFilterCallback implements QuickSearch.Callback {

        @Override
        public void quickSearchUpdate(String searchText) {
            FilterRepository.getInstance().setQuickSearchFilter(searchText);
            if (quickSearch != null && !quickSearch.isAlwaysShown()) {
                setSearchVisible(true);
            }
        }

        @Override
        public void showNextSelection(boolean forward) {
//         notificationTable.showNextSelection(forward);
        }

        @Override
        public String findMaxPrefix(String prefix) {
            return prefix;
        }

        @Override
        public void quickSearchConfirmed() {
        }

        @Override
        public void quickSearchCanceled() {
            FilterRepository.getInstance().setQuickSearchFilter(null);
        }
    }

setSearchVisible() hides/displays the quick filter text box as well as the filter result in the outline view. See how it makes use of usetQuickFilter and setQuickFilter as created in the previous article.

/**
  * @param visible if {@code true} then display the quick search panel
  */
public void setSearchVisible(boolean visible) {
   quickSearch.setAlwaysShown(visible);
   if (!visible) {
      unsetQuickFilter();
   } else if (FilterRepository.getInstance().isQuickSearchFilter()) {
      setQuickFilter(0, getQuickFilter(FilterRepository.getInstance().getQuickSearchFilter()));
   }
   Settings.setSearchVisible(visible);
}

Run the application and test the new functionality. Click on the QuickFilter button, enter a filter and see how the outline view is filtered while you type, and displays the rows that satisfy the filter. Click on the QuickFilter button to disable the quick filter. Display the quick filter again and delete it to see all the rows of the outline view. Nice!

"Figure 7 TodoRCP Application with Quick Filter"

Introduce a Filter Menu button

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo