Sortable and clickable lists with Apache Wicket

For my recent project I had to implement a sortable and clickable list. The project uses Apache Wicket as the front-end. It took me a while to get it all working and after some refactoring I was able to produce a generic solution to this problem. This solution is based on the following assumptions

  • The list to be sorted is based on a JPA entity and all the columns that need to be sortable are available as simple getter methods on the entity;
  • The entity has an id attribute, which uniquely identifies it;
  • The equals() and hasCode() methods of the entity will be overriden. Two entitiy instances will  be considered equal if their ids match;
  • The JavaScript jQuery library is available in the Wicket page displaying the list.

See the image below for an example of the eventual sortable and clickable list on which this blog is based.

The list shows the Account entity. All the columns are sortable and clicking on a row will open the url on which the hyperlink of the first column is based. There’s also a pagination link available in case the number of rows exceeds the number of rows displayed. In the remainder of this blog I’ll walk through all the necessary components.

Model

Interface ISortableEntity

For an entity to be able to be sorted in a list, I’ve defined the following interface:


package com.example.entity;

import java.math.BigDecimal;

public interface ISortableEntity {

 public BigDecimal getId();
 public void setId(BigDecimal id);
 public ISortableEntity newSearchInstance();

}

As you can see, the entity has to have an id attribute. It also has to implement the newSearchInstance() method which basically has to return a new instance of the entity class.

Entity Account

The list is based on an entity called com.example.entity.Account. The entity has to implement the ISortableEntity interface and has to override the equals() and hashCode() methods. As mentioned before two entities are considered equal if their ids match.

package com.example.entity;

import java.math.BigDecimal;
import javax.persistence.*;

@Entity
@Table(name = "ACCOUNTS")
@SequenceGenerator(allocationSize = 1, name = "account_id_generator", sequenceName = "accounts_id_seq")
public class Account implements java.io.Serializable, ISortableEntity {

	private static final long serialVersionUID = -1213397204916173215L;
	private BigDecimal id;
	private BigDecimal accountNumber;
	private String firstName;
	private String lastName;
	private String username;

	public Account() {
	}

	public Account(BigDecimal id) {
		this.id = id;
	}

	public Account(BigDecimal id, BigDecimal accountNumber, String firstName,
			String lastName, String username) {
		this.id = id;
		this.accountNumber = accountNumber;
		this.firstName = firstName;
		this.lastName = lastName;
		this.username = username;
	}

	@Id
	@GeneratedValue(generator = "account_id_generator")
	@Column(name = "ID", unique = true, nullable = false, precision = 22, scale = 0)
	public BigDecimal getId() {
		return this.id;
	}

	public void setId(BigDecimal id) {
		this.id = id;
	}

	@Column(name = "ACCOUNT_NUMBER", precision = 22, scale = 0)
	public BigDecimal getAccountNumber() {
		return this.accountNumber;
	}

	public void setAccountNumber(BigDecimal accountNumber) {
		this.accountNumber = accountNumber;
	}

	@Column(name = "FIRST_NAME", length = 100)
	public String getFirstName() {
		return this.firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	@Column(name = "LAST_NAME", length = 100)
	public String getLastName() {
		return this.lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	@Column(name = "USERNAME", length = 50)
	public String getUsername() {
		return this.username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}

	public ISortableEntity newSearchInstance() {
		ISortableEntity searchAccount = new Account();
		return searchAccount;
	}
}

View

For the view layer the sortable list of accounts will be displayed on a Wicket Panel. For this we’ll need a template and the corresponding Java class. For the list to be sortable we first have to extend the abstract class org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider. For one of its methods, we also need to extend the abstract class org.apache.wicket.model.LoadableDetachableModel. I’ve generified both of these subclasses so they only depend on the aforementioned interface ISortableEntity. The code for these classes is shown below.

SortableEntityProvider

package com.example.view.common;

import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;

import com.example.entity.ISortableEntity;

public class SortableEntityProvider<T extends ISortableEntity> extends
		SortableDataProvider<T> {
	private static final long serialVersionUID = -1646363525627999438L;
	private List<T> data;

	protected List<T> getData() {
		return data;
	}

	protected void setData(List<T> data) {
		this.data = data;
	}

	public SortableEntityProvider(List<T> data, String defaultSort) {
		super();
		setData(data);
		// set default sort
		setSort(defaultSort, true);
	}

	public Iterator<? extends T> iterator(int first, int count) {
		final SortParam sp = getSort();

		Collections.sort(getData(), getComparator(sp));

		return getData().subList(first, first + count).iterator();
	}

	public int size() {
		return getData().size();
	}

	public IModel<T> model(T object) {
		return new DetachableEntityModel<T>(object, getData());
	}

	protected Comparator<T> getComparator(final SortParam sp) {
		return new Comparator<T>() {
			public int compare(T arg0, T arg1) {

				int result;

				PropertyModel<Comparable> model0 = new PropertyModel<Comparable>(
						arg0, sp.getProperty());
				PropertyModel<Comparable> model1 = new PropertyModel<Comparable>(
						arg1, sp.getProperty());

				if (model0.getObject() == null && model1.getObject() == null) {
					result = 0;
				} else if (model0.getObject() == null) {
					result = -1;
				} else if (model1.getObject() == null) {
					result = 1;
				} else {
					result = model0.getObject().compareTo(model1.getObject());
				}

				if (!getSort().isAscending()) {
					result = -result;
				}

				return result;

			}
		};
	}
}

The constructor takes a list with all the rows displayed on the panel (usually a list of entities returned by a service calling a DAO) as the data parameter, along with a string representation of the attribute providing the default sort, eg. “firstName” for default sorting on the First Name of the Account.

DetachableEntityModel

package com.example.view.common;

import java.math.BigDecimal;
import java.util.List;

import org.apache.wicket.model.LoadableDetachableModel;

import com.example.entity.ISortableEntity;

public class DetachableEntityModel<T extends ISortableEntity> extends LoadableDetachableModel<T> {
	private static final long serialVersionUID = -2133620119433783150L;

	private final ISortableEntity entity;
	private final List<T> data;

	public DetachableEntityModel(ISortableEntity a, List<T> data) {
		this.entity = a;
		this.data = data;
	}

	public int hashCode() {
		return getId().hashCode();
	}

	public BigDecimal getId() {
		return entity.getId();
	}

	public boolean equals(final Object obj) {
		if (obj == this) {
			return true;
		} else if (obj == null) {
			return false;
		} else if (obj instanceof DetachableEntityModel) {
			DetachableEntityModel other = (DetachableEntityModel) obj;
			return other.getId() == getId();
		}
		return false;
	}

	protected T load() {
		ISortableEntity searchEntity = entity.newSearchInstance();
		searchEntity.setId(getId());
		return data.get(data.indexOf(searchEntity));
	}
}

AccountsPanel.html

<html>
<body>
<wicket:panel>
  <h2>Accounts</h2>
  <table class="clickable">
    <thead>
      <th wicket:id="orderByAccountNumber">[Account number]</th>
      <th wicket:id="orderByFirstName">[First name]</th>
      <th wicket:id="orderByLastName">[Last name]</th>
      <th wicket:id="orderByUsername">[Username]</th>
    </thead>
    <tbody>
      <tr wicket:id="accountRow">
        <td><a wicket:id="id" href="#"><span wicket:id="idSpan">[id]</span></a></td>
        <td wicket:id="firstName">[First name]</td>
        <td wicket:id="lastName">[Last name]</td>
        <td wicket:id="username">[Username]</td>
      </tr>
    </tbody>
  </table>
  <br />
  <span wicket:id="navigator">[dataview navigator]</span>
</wicket:panel>
</body>
</html>

The css class clickable will take care of the styling of the table and is coupled to the jQuery code (shown later on) that will implement the row clicks. A row click will make sure that the hyperlink coupled to the first column in the table will be clicked. The page also contains a component that will take care of the pagination.

AccountsPanel.java

package com.example.view.common;
....
public class AccountsPanel extends Panel {
 private static final long serialVersionUID = -395786600454932830L;

 @SpringBean
 AccountService accountService;

 public AccountsPanel(String id) {
 super(id);

 drawChildren();
 }

 private void drawChildren() {

 List<Account> accounts = accountService.getAccounts();

 SortableDataProvider<Account> dp = new SortableEntityProvider<Account>(
 accounts, "lastName");

 final DataView<Account> dataView = new DataView<Account>(
 "accountRow", dp) {
 private static final long serialVersionUID = 6364718764586838158L;

 @Override
 public void populateItem(final Item<Account> item) {
 item.add(new SimpleAttributeModifier("class",
 item.getIndex() % 2 == 0 ? "even" : "odd"));

 Link<Account> idLink = new Link<Account>("id") {
 private static final long serialVersionUID = -480222850475280108L;

 @Override
 public void onClick() {
 //TODO: Do stuff
 }
 };
 idLink.add(new Label("idSpan", item.getModelObject().getId()
 .toPlainString()));
 item.add(idLink);

 item.add(new Label("firstName", item.getModelObject()
 .getFirstName()));
 item.add(new Label("lastName", item.getModelObject()
 .getLastName()));
 item.add(new Label("username", item.getModelObject()
 .getUsername()));
 }
 };

 dataView.setItemsPerPage(5);

 addSorting(dp, dataView);

 add(dataView);

 add(new PagingNavigator("navigator", dataView));
 }

 private void addSorting(SortableDataProvider<Account> dp,
 final DataView<Account> dataView) {
 add(new SortableOrderBy<Account>("orderByAccountNumber", "id", dp,
 dataView));
 add(new SortableOrderBy<Account>("orderByLastName", "lastName", dp,
 dataView));
 add(new SortableOrderBy<Account>("orderByFirstName", "firstName", dp,
 dataView));
 add(new SortableOrderBy<Account>("orderByUsername", "username", dp,
 dataView));
 }

}
  • lines 6,7,17: Service (in this case a Spring Bean) that will cough up the list of accounts for display;
  • line 19,20: Here the SortableDataProvider is initialized with the accounts list. The accounts will be sorted on “lastName” by default;
  • line 22,23: The SortableDataProvider is fed to a DataView coupled to the accountRow table row wicket component;
  • line 36: This will be the code executed when the row is clicked;
  • line 52: The list displayed will contain 5 accounts;
  • line 54: Here the table row headers are added. They will be clickable for sorting the corresponding table column they belong to. I’ve created a small subclass for OrderByBorder, called SortableOrderBy so I don’t have to repeat the code that resets the pagination upon sort (the code will be provided below). Note that the second argument of the SortableOrderBy constructor is couple to the getter method on the Account entity. You can also apply sorting to properties of a related entity. Let’s presume that the Account entity has a relationship with an Address entity. If you would like to display say the city of the Address in the Accounts list and make it sortable, the second argument would be address.city ;
  • line 58: Here the paginator for the list is added to the panel.

SortableOrderBy.java

package com.example.view.common;

import org.apache.wicket.extensions.markup.html.repeater.data.sort.ISortStateLocator;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder;
import org.apache.wicket.markup.repeater.data.DataView;

public class SortableOrderBy<T> extends OrderByBorder {

  private static final long serialVersionUID = 5017011967951860645L;
  private final DataView<T> dataView;

  public SortableOrderBy(String id, String property,
      ISortStateLocator stateLocator, DataView<T> dataView) {
    super(id, property, stateLocator);
    this.dataView = dataView;
  }

  @Override
  protected void onSortChanged() {
    dataView.setCurrentPage(0);
  }
}

As I said, this is just a simple subclass to reset the current page to the first one after the sorting has been applied.

css

.clickable tr.even {
    background-color:#E9F9FF;
}

.clickable tr:hover {
    background-color: #F0F0F0;
}

.clickable td:hover {
    cursor: pointer;
}

I’m just showing the most important style elements. The above will make sure that:

  • the odd and even rows of the table are of different color;
  • the row that you hover over will change its color;
  • the arrow cursor will be replaced by a pointer cursor when you hover over a clickable row;

jQuery

The last piece of the puzzle is a small piece of jQuery code that will make sure the correct styling is applied and that a row click will lead to the clicking of the corresponding hyperlink programmatically.

$(document).ready(function() {

    $('.clickable tr').click(function() {
        var href = $(this).find("a").attr("href");
        if(href) {
            window.location = href;
        }
    });

})

And that’s all there is to it. With the code shown in this blog you can transform any list of entities into a sortable and clickable one.

Advertisements
%d bloggers like this: