A Pattern for Wicket Data Providers

Apache Wicket provides the AjaxFallbackDefaultDataTable1 for displaying a paged view of a large data set, and this component requires an implementation of ISortableDataProvider to populate the rows. This data provider has several responsibilities:

If you are satisfied with sorting on a single column at any time, the best approach is to extend the abstract class SortableDataProvider, which handles all the work involved in tracking the single sort property.

As the data provider must be Serializable, it cannot reference the database directly, but must find a database connection via some type of service locator. I’ve used Wicket-Spring to inject a repository object that handles all search and sorting logic. A single instance of the repository object can be shared between many data providers, so the search and sorting parameters have to be passed in with every request:

@Component
public class ClientSearchBackend  {

    public long count(@Nonnull ClientSearchParams filter) {
        // Carry out database query

        ....
    }

    public List<ClientData> list(long first, long count, 
                                 @Nonnull ClientSearchParams filter, 
                                 @Nonnull String sortparam, boolean ascending) {
        // Carry out database query

        ....
    }
}

The data provider is then only responsible for passing parameters to the repository. ClientSearchParams is a serializable bean class with get and set methods for each search field. Note that the ClientData returned from this data provider is Serializable and small, so can be stored directly in a model. More complex return types might benefit from wrapping the data in a LoadableDetachableModel.

public class ClientSearchProvider extends SortableDataProvider<ClientData, String> {

    private final ClientSearchParams filter = new ClientSearchParams ();

    @SpringBean
    private ClientSearchBackend backend;

    public ClientSearchProvider () {
        // As this is not a Component, it must trigger @SpringBean injection manually

        Injector.get().inject(this);
        setSort("accountNumber", SortOrder.ASCENDING);
    }

    public ClientSearchParams getFilter() {
        return filter;
    }

    @Override
    public Iterator<? extends ClientData> iterator(long first, long count) {
        // getSort() can be null if table unsorted. Default to 

        Optional<SortParam<String>> sort = Optional.ofNullable(getSort());
        return backend.list(first, count, filter,
                sort.map(SortParam::getProperty).orElse("accountNumber"),
                sort.map(SortParam::isAscending).orElse(true))
                .iterator();
    }

    @Override
    public long size() {
        return backend.count(filter);
    }

    @Override
    public IModel<ClientData> model(ClientData clientData) {
        return new Model<>(clientData);
    }
}

While the data provider could access a database connection directly via @SpringBean, this would make unit test of pages containing the data provider extremely difficult. Instead, unit tests can simply mock ClientSearchBackend and return fake data for the count and list calls.

For completeness, the Kotlin version of the data provider looks like this. Mostly similar, but handling the possibility of a null sort property is less verbose:

class ClientExtendedDataProvider : SortableDataProvider<ClientData, String>() {

    @SpringBean
    private val backend: ClientExtendedBackend? = null

    val filter = ClientExtendedFilter()

    init {
        Injector.get().inject(this)
        setSort("accountNumber", SortOrder.ASCENDING)
    }

    override fun iterator(first: Long, count: Long): Iterator<ClientData > {
        return backend!!.list(first, count, filter,
                sort?.property ?: "accountNumber",
                sort?.isAscending() ?: true)
                .iterator()
    }

    override fun size(): Long {
        return backend!!.count(filter)
    }

    override fun model(clientData: ClientData ): IModel<ClientData > {
        return Model(clientData)
    }
}

Originally published by Adrian Cox at https://adrianathumboldt.github.io/.

  1. Other repeating views are available.