Blog

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

Sergey Titov

In this post, I’m going to elaborate a bit on ZK, one of intriguing frameworks I worked with over the last seven months. I am not intended to write a lot about what it really is and what are the pros and cons of using it. To learn about such things, feel free to go to http://zkoss.org/product where you can find almost everything you need to know about ZK. Why did I say “almost”? In my 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.

The task

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 Web sites 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 which 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.

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. I 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. I am not going to describe here how it can be done, since—I 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. I am going to utilize this feature to avoid memory over-use.

So, now it is time to write the code. The following snippet 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 is an empty string. StringUtils is a utility class from Apache Commons. The only thing left is to implement ListModelFlyweight. It can be done like this:

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 my mind, it is user-friendly and simple. Well, what do you think this implementation?

1 Comment

Download Benchmarks and Research

© 2001 – 2018 Altoros