When data is stored in a database we want to be able to search for records. Searching is so common that DomUI has a special component that helps with creating database search screens: the SearchPanel. The Search Panel works on database objects, i.e. entity classes that are defined in Hibernate or JPA, or even with plain JDBC accessed objects (using DomUI's generic database layer). This means that working with the panel you stay inside the Java world.

Let's start with an example:

@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   lf.setClicked(a -> search(lf));

   lf.add().property("customer").control();        // Start with lookup by customer
   lf.add().property("total").control();           // Allow searching for a total
   lf.add().property("invoiceDate").control();     // And the date.

This example creates a search panel which searches for Invoice instances using the customer, total (amount) and invoiceDate properties. The actual searching and showing of the data is done by base class which will be shown at the end of this document.

This fragment creates the following UI (LIVE - click inside to play with it):

<iframe src="https://etc.to/demo/to.etc.domuidemo.pages.searchpanel.SearchPanelManual1.ui" width="1024" height="768"></iframe>

As can be seen, each property is presented on the form, in order. And for each property we have a special "control" which allows for input related to the type of the property. We see, in order:

The SearchPanel uses metadata to get the default label for properties, and it uses a registry of factories (LookupControlRegistry2) to find the best lookup control for a property, by type. The registry can be easily extended with your own lookup control factories.

You can control how data is shown using the builder pattern exhibited above. In that way you can change:

Using metadata with the panel

In the above example we specified what to search on by hand. This is often handy because it allows full control. But the form can also be populated automatically by using the metadata associated with the entity we look for. Take for example the following definition for Invoice's metadata:

@Table(name = "Invoice")
@SequenceGenerator(name = "sq", sequenceName = "invoice_sq")
@MetaObject(defaultColumns = {                      // 20180203 Must have metadata for SearchPanel/LookupForm tests.
   @MetaDisplayProperty(name = "customer.lastName", displayLength = 20)
   , @MetaDisplayProperty(name = "customer.firstName", displayLength = 10)
   , @MetaDisplayProperty(name = "invoiceDate")
   , @MetaDisplayProperty(name = "billingAddress", displayLength = 20)
   , @MetaDisplayProperty(name = "billingCity", displayLength = 10)
   , @MetaDisplayProperty(name = "total", displayLength = 10)
, searchProperties = {
   @MetaSearchItem(name = "invoiceDate")
   , @MetaSearchItem(name = "billingCity")
   , @MetaSearchItem(name = "customer")

The searchProperties above are the "default" properties to use inside the SearchPanel, and they take effect when no manual configuration of the SearchPanel is done, like this:

@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   lf.setClicked(a -> search(lf.getCriteria()));

In this case the metadata takes effect, resulting in:

Mixing metadata and user configuration

By default the SearchPanel does not use metadata when the panel is configured manually. To combine metadata with manually added controls call the following:


This adds the metadata to the definition as follows:

One word of warning though: it is dangerous to assume a lot about how the metadata for an object looks. So manipulating the result of the metadata a lot inside a form is madness: any time the metadata changes the form becomes unstable. If you need to make large changes to how a form would look when it only uses metadata consider configuring the thing completely - that makes you independent of metadata.

Customizing the SearchPanel

Using default values

To have default values for controls we use the builder. To define a default value we need to know the data type of the control that is being used for searching. This data type is often not the same as the data type of the field we search on! This can be seen in the examples above:

To set a default value you must use the data types that are actually used by the control or things will fail.

An example of using default values is this:

@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   lf.setClicked(a -> search(lf.getCriteria()));

   //-- Find customer by ID
   Customer defaultCustomer = getSharedContext().get(Customer.class, Long.valueOf(10));
   lf.add().property("customer").defaultValue(defaultCustomer).control(); // Default customer

   //-- Default the search total to >= 5.0
   NumberLookupValue nlv = new NumberLookupValue(QOperation.GE, BigDecimal.valueOf(5.0));
   lf.add().property("total").defaultValue(nlv).control();             // Allow searching for a total

   //-- Default the date to before 2010.
   DatePeriod period = new DatePeriod(null, DateUtil.dateFor(2010, 0, 1));
   lf.add().property("invoiceDate").defaultValue(period).control();         // And the date.

We use the appropriate data type for the controls being used and define the value for the control from there. The net result is that the form shows with default search values loaded:

Intermezzo: SearchPanel under the hood

To have a SearchPanel we need the following parts:

The earlier LookupForm (now deprecated) combined all of this into a single cruddy interface. This made it very hard to customize. The SearchPanel separates all these concerns making it easier to change.

Lookup Controls

The search starts with a Lookup control. A Lookup Control is some class implementing IControl<> which can be used to enter data for a lookup. Some data types (as we've seen above) require a special control to be made to handle the search, as their search is somehow special. But many other data types can be looked up with normal controls:

If you do not like the default controls used by the SearchPanel it is relatively easy to create your own: just create a new class implementing IControl<> and make it do what you want. This often also requires that you define the proper datatype for the control, which must be able to hold all of the information that you use to search for.

Examples of Lookup controls can be found by looking at DomUI's own implementations under the lookupcontrols package inside the searchpanel package.

Because LookupControls are just normal IControl<> instances it is very easy to manipulate them: you can add change listeners or play with their values just as you would do with "normal" controls. Making one search control "react" to changes of another is done by just adding a change listener!

Lookup Query Builders

Once you have a value from a Lookup Control we need to somehow translate that value into a part of a QCriteria query. This is the responsibility of the ILookupQueryBuilder instances which are defined as follows:

public interface ILookupQueryBuilder<D> {
   <T> LookupQueryBuilderResult appendCriteria(@Nonnull QCriteria<T> criteria, @Nullable D lookupValue);

The type D is defined as the type of the value of the associated LookupControl. Implementations of this interface must convert the value entered by the user and represented by the lookupValue into something edible inside the QCriteria instance passed to the method.

The simplest QueryBuilder implementation is ObjectLookupQueryBuilder which is defined as follows:

final public class ObjectLookupQueryBuilder<D> implements ILookupQueryBuilder<D> {
   private final String m_propertyName;

   public ObjectLookupQueryBuilder(String propertyName) {
      m_propertyName = requireNonNull(propertyName);

   @Override public <T> LookupQueryBuilderResult appendCriteria(QCriteria<T> criteria, @Nullable D value) {
      if(value == null || (value instanceof String && ((String) value).trim().length() == 0))
         return LookupQueryBuilderResult.EMPTY;       // Is okay but has no data

      // FIXME Handle minimal-size restrictions on input (search field metadata)
      //-- Put the value into the criteria..
      if(value instanceof String) {
         String str = (String) value;
         str = str.trim().replace("*", "%") + "%";        // FIXME Do not search with wildcard by default 8-(
         criteria.ilike(m_propertyName, str);
      } else {
         criteria.eq(m_propertyName, value);             // property == value
      return LookupQueryBuilderResult.VALID;

The thing works as follows:

Because the value type can be anything the Query Builder can built very complex queries to do the actual searching.

Using your own lookup components

It is quite simple to replace the default components by your own. Depending on what you want you use/create a lookup component and you use/create a QueryBuilder. As an example we can replace the customer lookup with a Combobox (bad idea) easily as follows:

@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   lf.setClicked(a -> search(lf.getCriteria()));

   //-- Create a combobox of customers
   QCriteria<Customer> q = QCriteria.create(Customer.class)   // All customers with last names starting with A
      .ilike("lastName", "B%");
   ComboLookup2<Customer> customerC = new ComboLookup2<>(q);
   customerC.setContentRenderer((node, value) -> node.add(value.getFirstName() + " " + value.getLastName()));


   lf.add().property("total").control();           // Allow searching for a total
   lf.add().property("invoiceDate").control();          // And the date.

In this case, because the control returns a value (Customer) that needs to be compared with equals we only pass a new control; the SearchPanel will use the ObjectLookupQueryBuilder to create the query. The net results looks like this:

When you make a control that has a more complex value you need to create the LookupBuilder too.

Comparison with LookupForm's components

LookupForm combined everything about a single search property in a single interface (ILookupControlInstance). This interface was responsible for everything: the control to use, the search to perform, how to render the control and its label and the shoe size of the builder. To customize this was hard because everything needed to be constructed as one class. The controls used by this code were not real IControl instances so manipulating them was very special. In addition because both controls and search code needed to be together there was no clear separation of tasks which again reduced reusability - and caused some quite bad code in the process.

The form that was constructed by the LookupForm was fixed as there was no reasonable way to change the layout without adding more crud to the LookupForm.

The SearchPanels separates all of this into separate well-defined and reusable parts, and delegates "special use" into special parts - that themselves then become reusable again.

Lookup Factories

When just defining properties to search for the SearchPanel will try to create the proper lookup controls and query builder by itself. It does that by asking the LookupControlRegistry2 class for control factories that can handle the specified property. A factory instance is defined as follows:

public interface ILookupFactory<D> {
   @Nonnull FactoryPair<D> createControl(@Nonnull SearchPropertyMetaModel spm);

and gets registered like this:

register(new DateLookupFactory2(), a -> Date.class.isAssignableFrom(a.getActualType()) ? 10 : 0);
register(new EnumAndBoolLookupFactory2<>(), LookupControlRegistry2::scoreEnumerable);
register(new NumberLookupFactory2(), pmm -> DomUtil.isIntegerType(pmm.getActualType()) || DomUtil.isRealType(pmm.getActualType()) || pmm.getActualType() == BigDecimal.class ? 10 : 0);
register(new RelationLookupFactory2<>(), pmm -> pmm.getRelationType() ==  PropertyRelationType.UP ? 10 : 0);
register(new RelationComboLookupFactory2<>(), pmm -> pmm.getRelationType() == PropertyRelationType.UP && Constants.COMPONENT_COMBO.equals(pmm.getComponentTypeHint()) ? 10 : 0);
register(new StringLookupFactory2<>(), pmm -> 1);        // Accept all

The factory is combined with a lambda that checks the characteristics of the property against whatever the factory would accept. This comparison returns a score. The factory returning the highest > 0 score is the one that is asked to create the control and the query factory.

This makes it very easy to create your own defaults for SearchPanel controls: just create a factory, register it and make it return the correct score when you recognise a property you'd want to handle.