PrimeFaces DataTable and server side pagination, filtering and sorting

Recently I’ve tried PrimeFaces 4x DataTable once again (after year or so working with Apache Wicket). One thing — I like it. It is now possible to implement scallable server side pagination, filtering and sorting logic without too much hacks

Fist of all let’s examine org.primefaces.model package in order to undestand how LazyDataModel<T> works. Concrete implementation of this abstract class will be extensively used in this post (because this is also a core class for DataTable as you can see from stock examples -> LazyCarDataModel.java)

PrimeFaces Models

As you can see from the picture above LazyDataModel<T> extends core javax.faces.model.DataModel<T> and adds sorting / filtering capabilities.

Let’s concentrate on this :

/*
 * Copyright 2009-2013 PrimeTek.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *...
*/
public abstract class LazyDataModel<T> extends DataModel<T> implements SelectableDataModel<T>, Serializable {

...

 public List<T> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String,String> filters) {
        throw new UnsupportedOperationException("Lazy loading is not implemented.");
    }

    public List<T> load(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String,String> filters) {
        throw new UnsupportedOperationException("Lazy loading is not implemented.");
    }

    public T getRowData(String rowKey) {
        throw new UnsupportedOperationException("getRowData(String rowKey) must be implemented when basic rowKey algorithm is not used.");
    }

    public Object getRowKey(T object) {
        throw new UnsupportedOperationException("getRowKey(T object) must be implemented when basic rowKey algorithm is not used.");
    }
...
}

This is completely fine. Even string based  Map<String,String> filters parameter(s) because you provide stings as filter value by default in the DataTable

But what to do if you have layered architecture? For example actual sorting is performed in the REST / SOA back-end? I’m strongly agains using front-end entities / libraries / packages inside back-end.

One possible approach is to introduce your own set of entities for server side handling:

Server side paging

The core of the back-end framework is PageableDataProvider<T> interface. Concrete implementation(s) of this interface can act like front-end proxies for retreival of data:

TableDataModel

So you DataTable in the view binds to the LazyStudentsDataModel – concrete implementation of TableDataModel. The trick is that TableDataModel handles conversion of the front-end  entities into server-side ones and manages “magic” rowCount property:

public abstract class TableDataModel<T> extends LazyDataModel<T> {

	private static final long serialVersionUID = -6074599821435569629L;

	private int rowCount;

	@Override
	public int getRowCount() {
		return rowCount;
	}

	public void setRowCount(int rowCount) {
		this.rowCount = rowCount;
	}

	@Override
	public List<T> load(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, String> filters) {
		throw new UnsupportedOperationException();
	}

	@Override
	public List<T> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, String> filters) {
		LoadOptions loadOptions = convert(first, pageSize, sortField, sortOrder, filters);
		PagedList<T> data = getData(loadOptions);

		Long longSize = Long.valueOf(data.getTotalSize());

		setRowCount(longSize.intValue());

		return data;
	}

	protected abstract PagedList<T> getData(LoadOptions loadOptions);

	private LoadOptions convert(int first, int pageSize, String sortField, SortOrder sortOrder,
			Map<String, String> filters) {
		List<SortMeta> sorts = new ArrayList<>();
		if (!StringUtils.isBlank(sortField)) {
			SortMeta sort = new SortMeta();
			sort.setSortField(sortField);
			sort.setSortOrder(sortOrder);
			sorts.add(sort);
		}

		return convert(first, pageSize, sorts, filters);
	}

	private LoadOptions convert(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, String> filters) {
		LoadOptions loadOptions = new LoadOptions();
		loadOptions.setSkipItems(first);
		loadOptions.setTakeItems(pageSize);

		List<SortOption> sorts = new ArrayList<>();
		for (int i = 0; i < multiSortMeta.size(); ++i) {
			SortMeta sortMeta = multiSortMeta.get(i);
			if (sortMeta != null) {
				String sortField = sortMeta.getSortField();
				if (!StringUtils.isBlank(sortField)) {
					SortOption sort = new SortOption();
					sort.setSortField(sortField);
					sort.setSortDirection(convert(sortMeta.getSortOrder()));
					sort.setRank(i);
					sorts.add(sort);
				}
			}
		}
		loadOptions.setSorts(sorts);

		List<String> filterFields = new ArrayList<>(filters.keySet());
		List<FilterOption> filterOptions = new ArrayList<>();
		for (String filterField : filterFields) {
			if (!StringUtils.isBlank(filterField)) {
				String filterValue = filters.get(filterField);
				FilterOption filterOption = new FilterOption();
				filterOption.setField(filterField);
				filterOption.setValue(filterValue);
				filterOptions.add(filterOption);
			}
		}
		loadOptions.setFilters(filterOptions);

		return loadOptions;
	}

	private SortDirection convert(SortOrder sortOrder) {
		switch (sortOrder) {
		case ASCENDING:
			return SortDirection.ASCENDING;
		case DESCENDING:
			return SortDirection.DESCENDING;
		case UNSORTED:
			return SortDirection.UNSORTED;
		default:
			throw new IllegalArgumentException("Unknown sort order");
		}
	}
}

So the concrete implementations of TableDataModel are becoming plain simple (and without tons of boilerplate code):

@Service
@SessionScope
public class LazyStudentsDataModel extends TableDataModel<Student> {

	private static final long serialVersionUID = -544764175489727755L;

	@Autowired
	private StudentsDataProvider dataProvider;

	@Override
	protected PagedList<Student> getData(LoadOptions loadOptions) {
		return dataProvider.getObjects(loadOptions);
	}

}

Easy isn’t it?

Your server-side data provider implementation may look like:

@Repository
@DefaultProfile
public class StudentsDataProviderImpl implements StudentsDataProvider {

	private static final Logger LOG = LoggerFactory.getLogger(StudentsDataProviderImpl.class);

	@Autowired
	private SessionFactory sessionFactory;

	@Autowired
	private MappingService mappingService;

	@Transactional
	@Override
	public PagedList<Student> getObjects(LoadOptions loadOptions) {
		Session session = sessionFactory.getCurrentSession();

		PagedList<Student> result = new PagedList<>();

		try {
			Criteria countCriteria = session.createCriteria(StudentEntity.class);
			fillCriteria(countCriteria, loadOptions, true);

			long rowsCount = (long) countCriteria.uniqueResult();

			if (rowsCount > 0) {
				Criteria criteria = session.createCriteria(StudentEntity.class);
				fillCriteria(criteria, loadOptions, false);

				List<StudentEntity> dataEntities = criteria.list();
				for (StudentEntity de : dataEntities) {
					Student student = mappingService.map(de, Student.class);
					result.add(student);
				}
			}

			result.setTotalSize(rowsCount);
		} catch (Exception ex) {
			LOG.error("Error loading students", ex);
		}

		return result;
	}

private void fillCriteria(Criteria source, LoadOptions loadOptions, boolean countOnly) {
		List<FilterOption> filters = loadOptions.getFilters();
		if (CollectionUtils.hasItems(filters)) {
			for (FilterOption f : filters) {
				source.add(Restrictions.eq(f.getField(), f.getValue()));
			}
		}

		List<SortOption> sorts = loadOptions.getSorts();
		Collections.sort(sorts);

		if (CollectionUtils.hasItems(sorts)) {
			for (SortOption s : sorts) {
				switch (s.getSortDirection()) {
				case ASCENDING:
					source.addOrder(Order.asc(s.getSortField()));
					break;
				case DESCENDING:
					source.addOrder(Order.desc(s.getSortField()));
					break;
				default:
					break;
				}
			}
		}

		if (countOnly) {
			source.setProjection(Projections.rowCount());
		} else {
			source.setFirstResult(loadOptions.getSkipItems());
			source.setMaxResults(loadOptions.getTakeItems());
		}
	}
}

The trick here is the usage of PagedList<T>.  First call to the database counts total number of records to be displayed in the front-end table based on LoadOptions (thus filtering, sorting, page size etc). Second call fetches data to be displayed only in one front-end data table “page”.

Advertisements

One Response to “PrimeFaces DataTable and server side pagination, filtering and sorting”

  1. mousavi Says:

    hi
    that,s very good code.
    can you put a sample project to use this code?


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: