Some Tricks on How to Optimize an Auto-Complete Combobox Using ZK and Java

by Sergey TitovJune 2, 2010
With code samples, we demonstrate how to implement an auto-complete combobox under a real-world scenario.

The task

We do not intend to write a lot about what the ZK framework really is and what the pros and cons of using it are. To learn about such things, feel free to go to the product’s website where you can find almost everything you need to know about ZK. Why did we say “almost?” In our opinion, the framework lacks detailed documentation.

However, let’s take a look at a real-world scenario—say, an auto-complete combobox—and describe its implementation.

Let’s assume that we want to implement a drop-down component that will be automatically filled with elements, as soon as we start typing something in it. You can find something like this, for example, on the websites that sell airline tickets. There, you start typing a departure or a destination city, and a component brings and displays the list of all available cities, which names begin with these letters. We are going to implement such a component using the ZK framework.

From the first glance, it may seem that we already have a component that provides exactly the same functionality: an auto-complete combobox that comes with ZK and takes a collection of items as an input and which auto-completeness is triggered by setting an auto-drop tag attribute value to true.

<combobox autodrop="true"/>

So far so good. If a collection is small enough, then everything is fine: the component displays items, the sun shines brightly, the customer is happy, and the developer may go to the dining room to have a cup of coffee. What if the collection passed to the component consists of hundreds or thousands of elements? Such approach results in two bottlenecks: component collection handling and database access. This is not what we want, since everyone knows how sluggish projects often end up. For this reason, we are going to play a bit with ZK and Java to enrich our combobox’s functionality.

 

The solution

We will start building our custom component with overriding the method that returns a portion of data to be displayed to the end user. The method can be found in the org.zkoss.zul.ListSubModel interface. Generally speaking, we can develop a class that implements this interface. However, we decided not to repeat something that has already been done before us and decided to utilize org.zkoss.zul.SimpleListModel. We should also admit that if we override getSubModel that fetches all the necessary items from the database, we will not need a “complete” combobox model. Passing the empty list to the model constructor will be enough, and it is also NullPointerException safe.

Additionally, let’s assume that every combobox has a query name that is utilized to fetch data from the database. It can be a name of a query that returns a list of cities, countries, pets, etc. The getSubModel method should use this name to get the chunk of data from the database that will be fed to the combobox. We’re not going to describe here how it can be done, since—we think—it is quite obvious. What we need so far is a query name and a number of items to be returned from the database.

The last trick is to use the Flyweight pattern. One can notice that the query name, starting symbols, and the size of the sample will uniquely define the collection—provided that the database is unchangeable. We’re going to utilize this feature to avoid memory overuse.

So, now it is time to write the code. The snippet below demonstrates what we have got so far.

private Combobox autodropCombobox;

// …

// Inside the initialization method
autodropCombobox.setModel(new SimpleListModel(Collections.emptyList()) {
    public ListModel getSubModel(Object value, int nRows) {
        if (value != null && StringUtils.isNotEmpty(queryName)) {
            String nameStartsWith = value.toString();
            List data = someService.getItems(itemsNumber, nameStartsWith);
            return ListModelFlyweight.create(data, nameStartsWith, queryName);
        }
        return ListModelFlyweight.create(Collections.emptyList(), EMPTY_STRING, queryName);
    }
});

autodropCombobox is a combobox we are working with, value includes starting symbols, queryName is a name of a query, itemsNumber is a number of items to fetch, EMPTY_STRING is a constant and an empty string. StringUtils is a utility class from Apache Commons. The only thing left is to implement ListModelFlyweight. Below you can find how it can be done.

public final class ListModelFlyweight extends SimpleListModel {
    private static final WeakHashMap< ListModelFlyweight, WeakReference< ListModelFlyweight >> FLYWEIGHT_DATA =
            new WeakHashMap< ListModelFlyweight, WeakReference< ListModelFlyweight >>();

    private final Long dataSize;

    private final String nameStartsWith;

    private final String queryName;

    public ListModelFlyweight (List modelData, String nameStartsWith, String queryName) {
        super(modelData);
        this.dataSize = modelData != null ? modelData.size() : 0L;
        this.nameStartsWith = nameStartsWith;
        this.queryName = queryName;
    }

    public static ListModelFlyweight create(List modelData, String nameStartsWith, String queryName) {
        ListModelFlyweight model = new ListModelFlyweight (modelData, nameStartsWith, queryName);
        if (!FLYWEIGHT_DATA.containsKey(model)) {
            FLYWEIGHT_DATA.put(model, new WeakReference< ListModelFlyweight >(model));
        }
        return FLYWEIGHT_DATA.get(model).get();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof ListModelFlyweight) {
            if (obj == this) {
                return true;
            }
            ListModelFlyweight other = (ListModelFlyweight) obj;
            return other.dataSize.equals(dataSize) && other.queryName.equals(queryName) &&
                    (nameStartsWith != null ? nameStartsWith.equals(other.nameStartsWith) : (other.nameStartsWith == null));
        }
        return false;
    }

    @Override
    public int hashCode() {
        return ((dataSize != null ? dataSize.hashCode() : 0) * 17 + (nameStartsWith != null ? nameStartsWith.hashCode() : 1) * 33 + queryName.hashCode() + 9);
    }
}

So, the result is the custom component that can be utilized across multiple cases. To our mind, it is user-friendly and simple.

What do you think about this implementation?

 

Further reading


The post was written by Sergey Titov (Senior Java Developer at Altoros) and edited by Olga Belokurskaya.