How To Load and Sort On Demand using Custom AbstractListModel and Sortable"
m (→Note from ZK) |
|||
(29 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
{{Template:Smalltalk_Author| | {{Template:Smalltalk_Author| | ||
|author=Edilson Alexandre Cuamba, Software Engineer, EXI - Engenharia e Comercialização de Sistemas Informáticos | |author=Edilson Alexandre Cuamba, Software Engineer, EXI - Engenharia e Comercialização de Sistemas Informáticos | ||
− | |date=November | + | |date=November 30, 2022 |
|version=ZK 9.6.1-Eval | |version=ZK 9.6.1-Eval | ||
}} | }} | ||
− | |||
− | |||
− | |||
− | |||
Line 13: | Line 9: | ||
Sometimes when you want to display data with a table structure, you use Listbox. This component is appropriate to it and works fine until your data gets huge and passes millions of rows; you will notice a little delay when you open the view with this component. For that reason, I needed to create a custom AbstractListModel that supports fetching only the data for the current page on the screen. | Sometimes when you want to display data with a table structure, you use Listbox. This component is appropriate to it and works fine until your data gets huge and passes millions of rows; you will notice a little delay when you open the view with this component. For that reason, I needed to create a custom AbstractListModel that supports fetching only the data for the current page on the screen. | ||
In this article, I will show you how to build your own CustomAbstractListModel<T> that supports pagination at the database layer. | In this article, I will show you how to build your own CustomAbstractListModel<T> that supports pagination at the database layer. | ||
+ | |||
Let’s see what we will build: | Let’s see what we will build: | ||
− | [[ | + | [[File:Loading&sorting.png|center]] |
+ | [https://i.imgur.com/EmHVXl5.gif External link to a GIF showing the application >>] | ||
+ | Let’s do it! | ||
− | + | = Dependencies = | |
− | + | To build this, we will need some functions. First is dependency injection (you can use other frameworks, but I personally like Spring) provided by Spring Framework. Second is the capacity to preserve the query structure and change the structure during the pagination. For this, I use a library called search-jpa-hibernate. Relax, and I will show you how to configure this! | |
− | To build this, we will need some functions. First is dependency injection (you can use other frameworks, but I personally like Spring) provided by Spring Framework. Second is the capacity to preserve the query structure | ||
This is my build.gradle file: | This is my build.gradle file: | ||
Line 98: | Line 96: | ||
Now let’s build our Custom AbstractListModel<T>. | Now let’s build our Custom AbstractListModel<T>. | ||
− | == | + | = Building a PaginatedListModel<T> = |
− | <syntaxhighlight lang= | + | First of all, I need to show you how a listbox works: |
− | public class | + | # Every Listbox is associated with a ListModel if you don’t set one using Listbox.setModel(ListModel<T> listModel), your listbox will create one implicitly for your Listbox, so we will use this function to tell our Listbox which ListModel<T> it must use. |
+ | # This ListModel<T> is an interface that has two important methods: | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | //Returns the value at the specified index. | ||
+ | public <E> getElementAt(i); | ||
+ | |||
+ | //Returns the length of the list. | ||
+ | public int getSize(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | These two methods are the most important methods of our PaginatedListModel<T>; our class will override these two methods to call the specific data for a new row directly from the database, so we will reduce the use of a lot of resources and increase the velocity of our application. | ||
+ | Let’s build this class. | ||
+ | |||
+ | == Building the class PaginatedListModel<T> == | ||
+ | Our class will be an implementation Sortable<T> and child of the class AbstractListModel<T> because we will need some methods that this class gives like getting the page size, getting the current page, and so on. | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | import com.eacuamba.loading_and_sorting.domain.service.Searchable; | ||
+ | import com.eacuamba.loading_and_sorting.helper.ApplicationContextHelper; | ||
+ | import com.googlecode.genericdao.search.Search; | ||
+ | import com.googlecode.genericdao.search.Sort; | ||
+ | import com.googlecode.genericdao.search.jpa.JPASearchFacade; | ||
+ | import org.apache.commons.lang3.ArrayUtils; | ||
+ | import org.zkoss.zk.ui.Desktop; | ||
+ | import org.zkoss.zk.ui.Executions; | ||
+ | import org.zkoss.zul.AbstractListModel; | ||
+ | import org.zkoss.zul.FieldComparator; | ||
+ | import org.zkoss.zul.event.ListDataEvent; | ||
+ | import org.zkoss.zul.ext.Sortable; | ||
+ | |||
+ | public class PaginatedListModel<T> extends AbstractListModel<T> implements Sortable<T> { | ||
+ | //this three (respectively) are from the library that I use to search data and preserve the query during the life of the listbox. | ||
+ | private final JPASearchFacade jpaSearchFacade; | ||
+ | private final Search search; | ||
+ | private final Sort[] sorts; | ||
+ | |||
+ | //stores the key of our cache; you don't want to hit the database twice to get the same data, right? | ||
+ | private final String CACHE_KEY = String.format("%s_cache", PaginatedListModel.class.getName()); | ||
+ | //stores the current object with the filters; | ||
+ | private final T t; | ||
+ | //start the size with null to know when to fetch or not. | ||
+ | private Integer size = null; | ||
+ | |||
+ | @SuppressWarnings({"unchecked"}) | ||
+ | public PaginatedListModel(T t, Class<T> tClass) { | ||
+ | //tying to find an instance of JPASearchFacade, we need to create this bean. | ||
+ | this.jpaSearchFacade = ApplicationContextHelper.getBean(JPASearchFacade.class); | ||
+ | //tying to find our Searchable class with the logic or filter if we have it. | ||
+ | this.search = ((Searchable<T>) ApplicationContextHelper.getBean(Searchable.class, t.getClass())).search(new Search(t.getClass()), t); | ||
+ | //reset the sorts; | ||
+ | this.sorts = null; | ||
+ | //set our class. | ||
+ | this.t = t; | ||
+ | //reset the cache | ||
+ | this.resetCache(); | ||
+ | } | ||
+ | |||
+ | @SuppressWarnings({"unchecked"}) | ||
+ | public PaginatedListModel(T t, Sort... sorts) { | ||
+ | this.jpaSearchFacade = ApplicationContextHelper.getBean(JPASearchFacade.class); | ||
+ | this.search = ((Searchable<T>) ApplicationContextHelper.getBean(Searchable.class, t.getClass())).search(new Search(t.getClass()), t); | ||
+ | this.sorts = sorts; | ||
+ | this.t = t; | ||
+ | this.resetCache(); | ||
+ | } | ||
+ | |||
+ | public PaginatedListModel(T t, Boolean multiple, Sort... sorts) { | ||
+ | this(t, sorts); | ||
+ | //permits your select multiple rows. | ||
+ | this.setMultiple(multiple); | ||
+ | } | ||
+ | |||
+ | // this method first try to get our cached Map<Integer, T>, if it finds it will try to get an element at a specific index, if not, it will hit the database, get the range of data, and then fill the cache and the cycle starts over again trying to find the data from the cache. | ||
+ | @Override | ||
+ | @SuppressWarnings({"unchecked"}) | ||
+ | public T getElementAt(int index) { | ||
+ | //getting the cache, it's a Map<Integer, T>, <index, our entity class> | ||
+ | Map<Integer, T> cacheMap = getCache(); | ||
+ | |||
+ | //trying to get the object at the specified position; | ||
+ | T target = cacheMap.get(index); | ||
+ | if (Objects.isNull(target)) { | ||
+ | List<T> page; | ||
+ | |||
+ | //check if the user set sorts; if not, make a common search, then put in the page List<T>; otherwise do the same without the sort. | ||
+ | //see the use of getActivePage() this will return the number of the current page, next, the use of getPageSize() it will return the size of page set at the .zul file. | ||
+ | if (ArrayUtils.isEmpty(sorts)) { | ||
+ | page = jpaSearchFacade.search(search.setPage(this.getActivePage()).setMaxResults(this.getPageSize())); | ||
+ | } else | ||
+ | page = jpaSearchFacade.search(search.setPage(this.getActivePage()).setMaxResults(this.getPageSize()).addSorts(sorts)); | ||
+ | |||
+ | //with the page insert the data in our cache, one by one, starting from the index the Listbox send to us. | ||
+ | int nextIndex = index; | ||
+ | for (T t : page) { | ||
+ | cacheMap.put(nextIndex++, t); | ||
+ | } | ||
+ | } else { | ||
+ | return target; | ||
+ | } | ||
+ | |||
+ | //returning the target if it was not in the cache; | ||
+ | target = cacheMap.get(index); | ||
+ | |||
+ | //check if the item was found. If not, throw an exception, sometimes it happens because of a change the number of data in the database, if so, you need to refresh the listbox!. | ||
+ | if (Objects.isNull(target)) { | ||
+ | throw new RuntimeException(String.format("The item at index %d was not found.", index)); | ||
+ | } else | ||
+ | return target; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public int getSize() { | ||
+ | //to avoid hitting the database too many times to get the same value (the number of rows returned). | ||
+ | if (Objects.isNull(size)) | ||
+ | return this.size = jpaSearchFacade.count(search); | ||
+ | return size; | ||
+ | } | ||
+ | |||
+ | //this method returns the cache if it exists or creates one if not | ||
+ | @SuppressWarnings({"unchecked"}) | ||
+ | private Map<Integer, T> getCache() { | ||
+ | Desktop desktop = Executions.getCurrent().getDesktop(); | ||
+ | Map<Integer, T> cacheMap = (Map<Integer, T>) desktop.getAttribute(this.CACHE_KEY); | ||
+ | if (Objects.isNull(cacheMap)) { | ||
+ | cacheMap = new HashMap<>(); | ||
+ | desktop.setAttribute(this.CACHE_KEY, cacheMap); | ||
+ | } | ||
+ | return cacheMap; | ||
+ | } | ||
+ | |||
+ | //this will destroy the data in the cache, get the cache then clear the data if the cache exists; | ||
+ | public void resetCache() { | ||
+ | this.size = null; | ||
+ | Desktop desktop = Executions.getCurrent().getDesktop(); | ||
+ | Map<Integer, T> cacheMap = (Map<Integer, T>) desktop.getAttribute(this.CACHE_KEY); | ||
+ | if(Objects.nonNull(cacheMap)) | ||
+ | cacheMap.clear(); | ||
+ | fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1); | ||
+ | } | ||
+ | |||
+ | //in this method the magic of sort in the database layer happen, it will implement the method sort from Sortable<T> interface and when the user click at the table header to sort it will receive the entity property to be used to sort and the direction, will save at our search object. | ||
+ | @Override | ||
+ | public void sort(Comparator<T> tComparator, boolean ascending) { | ||
+ | search.clearSorts(); | ||
+ | String rawOrderBy = ((FieldComparator) tComparator).getRawOrderBy(); | ||
+ | |||
+ | Arrays.stream(rawOrderBy.split(",")).map(String::trim).forEach(s -> { | ||
+ | search.addSort(s, !ascending, true); | ||
+ | }); | ||
− | public | + | this.resetCache(); |
− | + | this.clearSelection(); | |
+ | fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1); | ||
+ | } | ||
+ | |||
+ | |||
+ | @Override | ||
+ | public String getSortDirection(Comparator<T> cmpr) { | ||
+ | return null; | ||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | |||
− | |||
− | = Download = | + | == Let’s build our ApplicationContextHelper == |
− | If you want to check the running code please | + | To help out find beans. |
+ | |||
+ | You can see that I used a class called ApplicationContextHelper; this class is my helper to find beans inside the Spring context. Let me show how this class is: | ||
+ | <syntaxhighlight lang="java"> | ||
+ | import org.springframework.beans.BeansException; | ||
+ | import org.springframework.context.ApplicationContext; | ||
+ | import org.springframework.context.ApplicationContextAware; | ||
+ | import org.springframework.core.ResolvableType; | ||
+ | import org.springframework.stereotype.Component; | ||
+ | import org.yaml.snakeyaml.util.ArrayUtils; | ||
+ | |||
+ | //annotate it as a component, so spring can inject ApplicationContext in it using set method; | ||
+ | @Component | ||
+ | public class ApplicationContextHelper implements ApplicationContextAware { | ||
+ | private static ApplicationContext applicationContext; | ||
+ | @Override | ||
+ | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { | ||
+ | ApplicationContextHelper.applicationContext = applicationContext; | ||
+ | } | ||
+ | |||
+ | // this method receives a class and a type then find a bean of this class. With this type, we will be searching for something like CountrySearchable<Country>, country searchable implements a searchable interface. | ||
+ | public static <T> T getBean(Class<T> tClass, Class<?> type){ | ||
+ | System.out.printf("Trying to find a bean of class %s and type %s.%n", tClass.getName(), type.getName()); | ||
+ | String[] applicationContextBeanNamesForType = ApplicationContextHelper.applicationContext.getBeanNamesForType(ResolvableType.forClassWithGenerics(tClass, type)); | ||
+ | return (T)applicationContext.getBean(applicationContextBeanNamesForType[0], tClass); | ||
+ | } | ||
+ | |||
+ | public static <T> T getBean(Class<T> tClass){ | ||
+ | System.out.printf("Trying to find a bean named %s.%n", tClass.getName()); | ||
+ | return applicationContext.getBean(tClass); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Let’s build our bean JPASearchFacade == | ||
+ | Now we need to create a bean called JPASearchFacade, so we can find this class in every place, to use it to find beans with some signatures: | ||
+ | <syntaxhighlight lang="java"> | ||
+ | import com.googlecode.genericdao.search.jpa.JPAAnnotationMetadataUtil; | ||
+ | import com.googlecode.genericdao.search.jpa.JPASearchFacade; | ||
+ | import com.googlecode.genericdao.search.jpa.JPASearchProcessor; | ||
+ | import org.springframework.context.annotation.Bean; | ||
+ | import org.springframework.stereotype.Component; | ||
+ | |||
+ | import javax.persistence.EntityManager; | ||
+ | import javax.persistence.PersistenceContext; | ||
+ | |||
+ | @Component | ||
+ | public class JPAFacadeConfiguration { | ||
+ | @PersistenceContext | ||
+ | private EntityManager entityManager; | ||
+ | |||
+ | @Bean | ||
+ | public JPASearchFacade getJpaSearchFacade(){ | ||
+ | final JPASearchFacade jpaSearchFacade = new JPASearchFacade(); | ||
+ | final JPAAnnotationMetadataUtil jpaAnnotationMetadataUtil = new JPAAnnotationMetadataUtil(); | ||
+ | final JPASearchProcessor jpaSearchProcessor = new JPASearchProcessor(jpaAnnotationMetadataUtil); | ||
+ | jpaSearchFacade.setSearchProcessor(jpaSearchProcessor); | ||
+ | jpaSearchFacade.setEntityManager(entityManager); | ||
+ | return jpaSearchFacade; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Searchable<T> where filter logic dwell == | ||
+ | Now that you have these classes let’s build our view and use this new type of ListModel: | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | import com.googlecode.genericdao.search.Search; | ||
+ | |||
+ | public interface Searchable<T> { | ||
+ | // we need the search so we can apply filters to it. | ||
+ | Search search(Search search, T t); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | First, you gonna need the Implementation of Searchable. Let’s build it: | ||
+ | <syntaxhighlight lang="java"> | ||
+ | @Service | ||
+ | public class CitySearchable implements Searchable<City> { | ||
+ | |||
+ | @Override | ||
+ | public Search search(Search search, City city) { | ||
+ | //setting the type of result, set to auto, then clear old filters, add fetch to country so that it will execute only one query. | ||
+ | search.setResultMode(ISearch.RESULT_AUTO); | ||
+ | search.clearFilters(); | ||
+ | search.addFetch("country"); | ||
+ | |||
+ | //applying filters, 1st see if the name is not empty or null; if not, apply like filter with the right property. | ||
+ | if(StringUtils.isNotBlank(city.getName())){ | ||
+ | search.addFilter(Filter.ilike("name", city.getName())); | ||
+ | } | ||
+ | |||
+ | return search; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Our .zul file == | ||
+ | <syntaxhighlight lang="java"> | ||
+ | <listbox id="listbox_city" mold="paging" pageSize="6"> | ||
+ | <listhead> | ||
+ | <listheader label="Identifier" hflex="min"/> | ||
+ | <listheader label="Name" sort="auto(name)"/> | ||
+ | <listheader label="State Code" hflex="min"/> | ||
+ | <listheader label="Country Name" sort="auto(country.name)"/> | ||
+ | <listheader label="Phone Code" hflex="min"/> | ||
+ | <listheader label="Currency Name"/> | ||
+ | <listheader label="Region (Continent)" hflex="min"/> | ||
+ | </listhead> | ||
+ | |||
+ | <template name="model"> | ||
+ | <listitem value="${each}"> | ||
+ | <listcell label="${each.id}"/> | ||
+ | <listcell label="${each.name}"/> | ||
+ | <listcell label="${each.stateCode}"/> | ||
+ | <listcell label="${each.country.name}"/> | ||
+ | <listcell label="${each.country.phoneCode}"/> | ||
+ | <listcell label="${each.country.currencyName}"/> | ||
+ | <listcell label="${each.country.region}"/> | ||
+ | </listitem> | ||
+ | </template> | ||
+ | </listbox> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Our controller == | ||
+ | This controller will hold the logic behind the view. | ||
+ | <syntaxhighlight lang="java"> | ||
+ | |||
+ | @VariableResolver(org.zkoss.zkplus.spring.DelegatingVariableResolver.class) | ||
+ | public class CityPaginatedController extends SelectorComposer<Vlayout> { | ||
+ | |||
+ | @WireVariable | ||
+ | private CityRepository cityRepository; | ||
+ | |||
+ | @Wire | ||
+ | private Listbox listbox_city; | ||
+ | |||
+ | @Override | ||
+ | public ComponentInfo doBeforeCompose(Page page, Component parent, ComponentInfo compInfo) { | ||
+ | return super.doBeforeCompose(page, parent, compInfo); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void doAfterCompose(Vlayout comp) throws Exception { | ||
+ | super.doAfterCompose(comp); | ||
+ | |||
+ | //create an instance of PaginatedListModel of city | ||
+ | final PaginatedListModel<City> cityPaginatedListModel = new PaginatedListModel<>(City.builder().build()); | ||
+ | |||
+ | //associeting with our listModel. | ||
+ | listbox_city.setModel(cityPaginatedListModel); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | = Download = | ||
+ | If you want to check the running code, please access the repository at github.com. | ||
https://github.com/EACUAMBA/loading_and_sorting_on__demand | https://github.com/EACUAMBA/loading_and_sorting_on__demand | ||
= Summary = | = Summary = | ||
− | + | In this article, you saw that it is easy to create a custom ListModel that will handle queries and sort at database layer. | |
+ | As you can see, you have an AbstractListModel, and you still have the capabilities to select, get selection, and perform other things ALM<T> can do. | ||
+ | |||
+ | Please give it a try. | ||
+ | |||
+ | = Note from ZK = | ||
+ | Thanks to community user Edilson for writing this smalltalk and sharing his experience with everyone. If you also wish to contribute your work, don't hesitate to get in touch with us at info@zkoss.org. | ||
=Comments= | =Comments= |
Latest revision as of 16:14, 30 November 2022
Edilson Alexandre Cuamba, Software Engineer, EXI - Engenharia e Comercialização de Sistemas Informáticos
November 30, 2022
ZK 9.6.1-Eval
Overview
Sometimes when you want to display data with a table structure, you use Listbox. This component is appropriate to it and works fine until your data gets huge and passes millions of rows; you will notice a little delay when you open the view with this component. For that reason, I needed to create a custom AbstractListModel that supports fetching only the data for the current page on the screen. In this article, I will show you how to build your own CustomAbstractListModel<T> that supports pagination at the database layer.
Let’s see what we will build:
External link to a GIF showing the application >>
Let’s do it!
Dependencies
To build this, we will need some functions. First is dependency injection (you can use other frameworks, but I personally like Spring) provided by Spring Framework. Second is the capacity to preserve the query structure and change the structure during the pagination. For this, I use a library called search-jpa-hibernate. Relax, and I will show you how to configure this!
This is my build.gradle file: https://github.com/EACUAMBA/loading_and_sorting_on__demand/blob/main/build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.5.12")
}
}
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'org.springframework.boot'
apply plugin: "idea"
idea{
module {
setDownloadSources(true)
setDownloadJavadoc(true)
}
}
repositories {
mavenLocal()
maven { url "https://mavensync.zkoss.org/maven2" }
maven { url "https://mavensync.zkoss.org/eval" }
mavenCentral()
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
ext {
zkspringbootVersion = '2.5.12'
springbootVersion = '2.5.12'
zkspring = '4.0.0'
zkVersion = '9.6.0'
zatsVersion = '3.0.0'
junitVersion = '4.13.1'
searchJPAHibernateVersion = '1.2.0'
lombokVersion = '1.18.24'
mysqlVersion = '8.0.31'
javaxServerletAPIVersion='4.0.1'
commonsLang3Version='3.12.0'
}
configurations.testImplementation {
// conflicts with ZATS (which is using jetty)
exclude module: "spring-boot-starter-tomcat"
}
dependencies {
implementation ("org.zkoss.zkspringboot:zkspringboot-starter:${zkspringbootVersion}")
providedRuntime("javax.servlet:javax.servlet-api:${javaxServerletAPIVersion}")
implementation ("org.zkoss.zk:zkspring-core:${zkspring}")
implementation("org.zkoss.zk:zkplus:${zkVersion}")
implementation ("org.springframework.boot:spring-boot-starter-data-jpa:${springbootVersion}")
compileOnly ("org.springframework.boot:spring-boot-devtools:${springbootVersion}")
implementation ("com.googlecode.genericdao:search-jpa-hibernate:${searchJPAHibernateVersion}")
implementation("mysql:mysql-connector-java:${mysqlVersion}")
compileOnly("org.projectlombok:lombok:${lombokVersion}")
annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
testImplementation "org.springframework.boot:spring-boot-starter-test:${springbootVersion}"
implementation("org.apache.commons:commons-lang3:${commonsLang3Version}")
testImplementation "org.zkoss.zats:zats-mimic-ext96:${zatsVersion}"
testImplementation "junit:junit:${junitVersion}"
}
These are some of the dependencies I like to use, so feel free to remove the ones you don't need. Now let’s build our Custom AbstractListModel<T>.
Building a PaginatedListModel<T>
First of all, I need to show you how a listbox works:
- Every Listbox is associated with a ListModel if you don’t set one using Listbox.setModel(ListModel<T> listModel), your listbox will create one implicitly for your Listbox, so we will use this function to tell our Listbox which ListModel<T> it must use.
- This ListModel<T> is an interface that has two important methods:
//Returns the value at the specified index.
public <E> getElementAt(i);
//Returns the length of the list.
public int getSize();
These two methods are the most important methods of our PaginatedListModel<T>; our class will override these two methods to call the specific data for a new row directly from the database, so we will reduce the use of a lot of resources and increase the velocity of our application. Let’s build this class.
Building the class PaginatedListModel<T>
Our class will be an implementation Sortable<T> and child of the class AbstractListModel<T> because we will need some methods that this class gives like getting the page size, getting the current page, and so on.
import com.eacuamba.loading_and_sorting.domain.service.Searchable;
import com.eacuamba.loading_and_sorting.helper.ApplicationContextHelper;
import com.googlecode.genericdao.search.Search;
import com.googlecode.genericdao.search.Sort;
import com.googlecode.genericdao.search.jpa.JPASearchFacade;
import org.apache.commons.lang3.ArrayUtils;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zul.AbstractListModel;
import org.zkoss.zul.FieldComparator;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.Sortable;
public class PaginatedListModel<T> extends AbstractListModel<T> implements Sortable<T> {
//this three (respectively) are from the library that I use to search data and preserve the query during the life of the listbox.
private final JPASearchFacade jpaSearchFacade;
private final Search search;
private final Sort[] sorts;
//stores the key of our cache; you don't want to hit the database twice to get the same data, right?
private final String CACHE_KEY = String.format("%s_cache", PaginatedListModel.class.getName());
//stores the current object with the filters;
private final T t;
//start the size with null to know when to fetch or not.
private Integer size = null;
@SuppressWarnings({"unchecked"})
public PaginatedListModel(T t, Class<T> tClass) {
//tying to find an instance of JPASearchFacade, we need to create this bean.
this.jpaSearchFacade = ApplicationContextHelper.getBean(JPASearchFacade.class);
//tying to find our Searchable class with the logic or filter if we have it.
this.search = ((Searchable<T>) ApplicationContextHelper.getBean(Searchable.class, t.getClass())).search(new Search(t.getClass()), t);
//reset the sorts;
this.sorts = null;
//set our class.
this.t = t;
//reset the cache
this.resetCache();
}
@SuppressWarnings({"unchecked"})
public PaginatedListModel(T t, Sort... sorts) {
this.jpaSearchFacade = ApplicationContextHelper.getBean(JPASearchFacade.class);
this.search = ((Searchable<T>) ApplicationContextHelper.getBean(Searchable.class, t.getClass())).search(new Search(t.getClass()), t);
this.sorts = sorts;
this.t = t;
this.resetCache();
}
public PaginatedListModel(T t, Boolean multiple, Sort... sorts) {
this(t, sorts);
//permits your select multiple rows.
this.setMultiple(multiple);
}
// this method first try to get our cached Map<Integer, T>, if it finds it will try to get an element at a specific index, if not, it will hit the database, get the range of data, and then fill the cache and the cycle starts over again trying to find the data from the cache.
@Override
@SuppressWarnings({"unchecked"})
public T getElementAt(int index) {
//getting the cache, it's a Map<Integer, T>, <index, our entity class>
Map<Integer, T> cacheMap = getCache();
//trying to get the object at the specified position;
T target = cacheMap.get(index);
if (Objects.isNull(target)) {
List<T> page;
//check if the user set sorts; if not, make a common search, then put in the page List<T>; otherwise do the same without the sort.
//see the use of getActivePage() this will return the number of the current page, next, the use of getPageSize() it will return the size of page set at the .zul file.
if (ArrayUtils.isEmpty(sorts)) {
page = jpaSearchFacade.search(search.setPage(this.getActivePage()).setMaxResults(this.getPageSize()));
} else
page = jpaSearchFacade.search(search.setPage(this.getActivePage()).setMaxResults(this.getPageSize()).addSorts(sorts));
//with the page insert the data in our cache, one by one, starting from the index the Listbox send to us.
int nextIndex = index;
for (T t : page) {
cacheMap.put(nextIndex++, t);
}
} else {
return target;
}
//returning the target if it was not in the cache;
target = cacheMap.get(index);
//check if the item was found. If not, throw an exception, sometimes it happens because of a change the number of data in the database, if so, you need to refresh the listbox!.
if (Objects.isNull(target)) {
throw new RuntimeException(String.format("The item at index %d was not found.", index));
} else
return target;
}
@Override
public int getSize() {
//to avoid hitting the database too many times to get the same value (the number of rows returned).
if (Objects.isNull(size))
return this.size = jpaSearchFacade.count(search);
return size;
}
//this method returns the cache if it exists or creates one if not
@SuppressWarnings({"unchecked"})
private Map<Integer, T> getCache() {
Desktop desktop = Executions.getCurrent().getDesktop();
Map<Integer, T> cacheMap = (Map<Integer, T>) desktop.getAttribute(this.CACHE_KEY);
if (Objects.isNull(cacheMap)) {
cacheMap = new HashMap<>();
desktop.setAttribute(this.CACHE_KEY, cacheMap);
}
return cacheMap;
}
//this will destroy the data in the cache, get the cache then clear the data if the cache exists;
public void resetCache() {
this.size = null;
Desktop desktop = Executions.getCurrent().getDesktop();
Map<Integer, T> cacheMap = (Map<Integer, T>) desktop.getAttribute(this.CACHE_KEY);
if(Objects.nonNull(cacheMap))
cacheMap.clear();
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
//in this method the magic of sort in the database layer happen, it will implement the method sort from Sortable<T> interface and when the user click at the table header to sort it will receive the entity property to be used to sort and the direction, will save at our search object.
@Override
public void sort(Comparator<T> tComparator, boolean ascending) {
search.clearSorts();
String rawOrderBy = ((FieldComparator) tComparator).getRawOrderBy();
Arrays.stream(rawOrderBy.split(",")).map(String::trim).forEach(s -> {
search.addSort(s, !ascending, true);
});
this.resetCache();
this.clearSelection();
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
@Override
public String getSortDirection(Comparator<T> cmpr) {
return null;
}
}
Let’s build our ApplicationContextHelper
To help out find beans.
You can see that I used a class called ApplicationContextHelper; this class is my helper to find beans inside the Spring context. Let me show how this class is:
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.util.ArrayUtils;
//annotate it as a component, so spring can inject ApplicationContext in it using set method;
@Component
public class ApplicationContextHelper implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHelper.applicationContext = applicationContext;
}
// this method receives a class and a type then find a bean of this class. With this type, we will be searching for something like CountrySearchable<Country>, country searchable implements a searchable interface.
public static <T> T getBean(Class<T> tClass, Class<?> type){
System.out.printf("Trying to find a bean of class %s and type %s.%n", tClass.getName(), type.getName());
String[] applicationContextBeanNamesForType = ApplicationContextHelper.applicationContext.getBeanNamesForType(ResolvableType.forClassWithGenerics(tClass, type));
return (T)applicationContext.getBean(applicationContextBeanNamesForType[0], tClass);
}
public static <T> T getBean(Class<T> tClass){
System.out.printf("Trying to find a bean named %s.%n", tClass.getName());
return applicationContext.getBean(tClass);
}
}
Let’s build our bean JPASearchFacade
Now we need to create a bean called JPASearchFacade, so we can find this class in every place, to use it to find beans with some signatures:
import com.googlecode.genericdao.search.jpa.JPAAnnotationMetadataUtil;
import com.googlecode.genericdao.search.jpa.JPASearchFacade;
import com.googlecode.genericdao.search.jpa.JPASearchProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Component
public class JPAFacadeConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPASearchFacade getJpaSearchFacade(){
final JPASearchFacade jpaSearchFacade = new JPASearchFacade();
final JPAAnnotationMetadataUtil jpaAnnotationMetadataUtil = new JPAAnnotationMetadataUtil();
final JPASearchProcessor jpaSearchProcessor = new JPASearchProcessor(jpaAnnotationMetadataUtil);
jpaSearchFacade.setSearchProcessor(jpaSearchProcessor);
jpaSearchFacade.setEntityManager(entityManager);
return jpaSearchFacade;
}
}
Searchable<T> where filter logic dwell
Now that you have these classes let’s build our view and use this new type of ListModel:
import com.googlecode.genericdao.search.Search;
public interface Searchable<T> {
// we need the search so we can apply filters to it.
Search search(Search search, T t);
}
First, you gonna need the Implementation of Searchable. Let’s build it:
@Service
public class CitySearchable implements Searchable<City> {
@Override
public Search search(Search search, City city) {
//setting the type of result, set to auto, then clear old filters, add fetch to country so that it will execute only one query.
search.setResultMode(ISearch.RESULT_AUTO);
search.clearFilters();
search.addFetch("country");
//applying filters, 1st see if the name is not empty or null; if not, apply like filter with the right property.
if(StringUtils.isNotBlank(city.getName())){
search.addFilter(Filter.ilike("name", city.getName()));
}
return search;
}
}
Our .zul file
<listbox id="listbox_city" mold="paging" pageSize="6">
<listhead>
<listheader label="Identifier" hflex="min"/>
<listheader label="Name" sort="auto(name)"/>
<listheader label="State Code" hflex="min"/>
<listheader label="Country Name" sort="auto(country.name)"/>
<listheader label="Phone Code" hflex="min"/>
<listheader label="Currency Name"/>
<listheader label="Region (Continent)" hflex="min"/>
</listhead>
<template name="model">
<listitem value="${each}">
<listcell label="${each.id}"/>
<listcell label="${each.name}"/>
<listcell label="${each.stateCode}"/>
<listcell label="${each.country.name}"/>
<listcell label="${each.country.phoneCode}"/>
<listcell label="${each.country.currencyName}"/>
<listcell label="${each.country.region}"/>
</listitem>
</template>
</listbox>
Our controller
This controller will hold the logic behind the view.
@VariableResolver(org.zkoss.zkplus.spring.DelegatingVariableResolver.class)
public class CityPaginatedController extends SelectorComposer<Vlayout> {
@WireVariable
private CityRepository cityRepository;
@Wire
private Listbox listbox_city;
@Override
public ComponentInfo doBeforeCompose(Page page, Component parent, ComponentInfo compInfo) {
return super.doBeforeCompose(page, parent, compInfo);
}
@Override
public void doAfterCompose(Vlayout comp) throws Exception {
super.doAfterCompose(comp);
//create an instance of PaginatedListModel of city
final PaginatedListModel<City> cityPaginatedListModel = new PaginatedListModel<>(City.builder().build());
//associeting with our listModel.
listbox_city.setModel(cityPaginatedListModel);
}
}
Download
If you want to check the running code, please access the repository at github.com. https://github.com/EACUAMBA/loading_and_sorting_on__demand
Summary
In this article, you saw that it is easy to create a custom ListModel that will handle queries and sort at database layer. As you can see, you have an AbstractListModel, and you still have the capabilities to select, get selection, and perform other things ALM<T> can do.
Please give it a try.
Note from ZK
Thanks to community user Edilson for writing this smalltalk and sharing his experience with everyone. If you also wish to contribute your work, don't hesitate to get in touch with us at info@zkoss.org.
Comments
Copyright © {{{name}}}. This article is licensed under GNU Free Documentation License. |
</includeonly>