In this example we are going to build a simple search page using ZK framework and Spring Boot. We are going to use the latest available version of Spring Boot (3.0.0) and ZK Framework (9.6.0.2). So without taking more time let’s start by creating a new spring boot project with the following pom.xml
. You can create the initial project using your IDE or spring initializr.
Create a Spring Boot project and add the following dependencies:
zkspringboot-starter
zkplus
spring-boot-devtools
spring-boot-starter-data-jpa
mysql-connector-j
lombok
The pom.xml File
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.kodejava</groupId>
<artifactId>kodejava-zk-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>kodejava-zk-search</name>
<description>kodejava-zk-search</description>
<properties>
<java.version>17</java.version>
<zkspringboot.version>3.0.0</zkspringboot.version>
<zk.version.jakarta>9.6.0.2-jakarta</zk.version.jakarta>
<zk.version>9.6.0.2</zk.version>
</properties>
<dependencies>
<dependency>
<groupId>org.zkoss.zkspringboot</groupId>
<artifactId>zkspringboot-starter</artifactId>
<type>pom</type>
<version>${zkspringboot.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkplus</artifactId>
<version>${zk.version.jakarta}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>ZK CE</id>
<name>ZK CE Repository</name>
<url>https://mavensync.zkoss.org/maven2</url>
</repository>
<repository>
<id>ZK EVAL</id>
<name>ZK Evaluation Repository</name>
<url>https://mavensync.zkoss.org/eval</url>
</repository>
</repositories>
</project>
application.properties File
This properties file configure ZK application homepage and the prefix where the zul files are located. We also configure the datasource to our application database.
zk.homepage=label
zk.zul-view-resolver-prefix=/zul
zk.resource-uri=/zkres
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/musicdb
spring.datasource.username=root
spring.datasource.password=
spring.jpa.properties.hibernate.hbm2ddl.auto=create
Label.java Entity Definition
An entity that represent out record label with just two property of id
and name
. Getters and setters are generated by Lombok library, it also generated to equals()
and hashcode()
method, and also the toString()
method.
package org.kodejava.zk.entity;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
public class Label {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
The LabelRepository.java definition.
Create the LabelRepository
which extends the JpaRepository
and JpaSpecificationExecutor
interfaces.
package org.kodejava.zk.repository;
import org.kodejava.zk.entity.Label;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface LabelRepository extends JpaRepository<Label, Long>, JpaSpecificationExecutor<Label> {
}
AbstractSearchController.java a base search controller.
A base class that we can use to implements all the search page in an application. Basically it provides the method to search our application data. It defines a couple of abstract method that need to be implemented by the search controller classes such as what repository to use and the specification for searching the data. We can also define the default sort column and the direction of the data sorting.
package org.kodejava.zk.controller;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.VariableResolver;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Listbox;
@VariableResolver(org.zkoss.zkplus.spring.DelegatingVariableResolver.class)
public abstract class AbstractSearchController<T> extends SelectorComposer<Component> {
@Wire
protected Listbox listbox;
public abstract JpaSpecificationExecutor<T> getRepository();
public abstract Specification<T> getSpecification();
public abstract String getCacheKey();
protected String getDefaultSortColumn() {
return "id";
}
protected Sort.Direction getDefaultSortDirection() {
return Sort.Direction.ASC;
}
protected boolean getMultiple() {
return false;
}
@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
search();
}
@Listen("onClick=#searchButton")
public void search() {
listbox.setVisible(true);
SearchListModel<T> model = new SearchListModel<>(getRepository(), getSpecification(), getCacheKey());
model.setMultiple(getMultiple());
model.setDefaultSortColumn(getDefaultSortColumn());
model.setDefaultSortDirection(getDefaultSortDirection());
listbox.setModel(model);
listbox.setActivePage(0);
}
@Listen("onOK=#searchForm")
public void onEnterPressed(Event event) {
search();
}
public int getPageSize() {
return SearchListModel.PAGE_SIZE;
}
}
SearchListModel.java
An implementation of ListModel
, this class will query the database using the provided repository and specification. It read data page-by-page and cache it so when we navigating the Listbox
page it doesn’t read the data that have already been cached.
package org.kodejava.zk.controller;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zul.FieldComparator;
import org.zkoss.zul.ListModelList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
public class SearchListModel<T> extends ListModelList<T> {
public static final int PAGE_SIZE = 5;
private final JpaSpecificationExecutor<T> repository;
private final String cacheKey;
private long totalElements;
private Comparator<T> comparator;
private boolean ascending = false;
private final Specification<T> specification;
private Sort.Direction defaultSortDirection = Sort.Direction.ASC;
private String defaultSortColumn = "id";
public SearchListModel(JpaSpecificationExecutor<T> repository, Specification<T> specification, String cacheKey) {
this.repository = repository;
this.specification = specification;
this.cacheKey = cacheKey;
this.totalElements = repository.count(specification);
}
@Override
public T getElementAt(int index) {
Map<Integer, T> cache = getCache();
T target = cache.get(index);
if (target == null) {
Sort sort = Sort.by(getDefaultSortDirection(), getDefaultSortColumn());
if (comparator != null) {
FieldComparator fieldComparator = (FieldComparator) comparator;
String orderBy = fieldComparator.getRawOrderBy();
sort = Sort.by(ascending ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy);
}
Page<T> pageResult = repository.findAll(specification, PageRequest.of(getPage(index), PAGE_SIZE, sort));
totalElements = pageResult.getTotalElements();
int indexKey = index;
for (T t : pageResult.toList()) {
cache.put(indexKey, t);
indexKey++;
}
} else {
return target;
}
target = cache.get(index);
if (target == null) {
throw new RuntimeException("element at " + index + " cannot be found in the database.");
} else {
return target;
}
}
@Override
public int getSize() {
return (int) totalElements;
}
@Override
public void sort(Comparator<T> comparator, boolean ascending) {
super.sort(comparator, ascending);
this.comparator = comparator;
this.ascending = ascending;
}
@SuppressWarnings("unchecked")
private Map<Integer, T> getCache() {
Execution execution = Executions.getCurrent();
Map<Integer, T> cache = (Map<Integer, T>) execution.getAttribute(cacheKey);
if (cache == null) {
cache = new HashMap<>();
execution.setAttribute(cacheKey, cache);
}
return cache;
}
private int getPage(int index) {
if (index != 0) {
return index / PAGE_SIZE;
}
return index;
}
public Sort.Direction getDefaultSortDirection() {
return defaultSortDirection;
}
public void setDefaultSortDirection(Sort.Direction defaultSortDirection) {
this.defaultSortDirection = defaultSortDirection;
}
public String getDefaultSortColumn() {
return defaultSortColumn;
}
public void setDefaultSortColumn(String defaultSortColumn) {
this.defaultSortColumn = defaultSortColumn;
}
}
LabelSearchController.java
Our label search page controller which extends from AbstractSearchController
class. We provide the LabelRepository
and the Specification
to filter the data.
package org.kodejava.zk.controller;
import jakarta.persistence.criteria.Predicate;
import org.kodejava.zk.entity.Label;
import org.kodejava.zk.repository.LabelRepository;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.zkoss.zk.ui.select.annotation.VariableResolver;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zk.ui.select.annotation.WireVariable;
import org.zkoss.zul.Textbox;
import java.util.ArrayList;
import java.util.List;
@VariableResolver(org.zkoss.zkplus.spring.DelegatingVariableResolver.class)
public class LabelSearchController extends AbstractSearchController<Label> {
@WireVariable
private LabelRepository labelRepository;
@Wire
private Textbox labelNameTextbox;
@Override
public JpaSpecificationExecutor<Label> getRepository() {
return labelRepository;
}
@Override
public Specification<Label> getSpecification() {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
String labelName = labelNameTextbox.getValue();
if (!labelName.isBlank()) {
predicates.add(criteriaBuilder.like(root.get("name"), "%" + labelName + "%"));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
@Override
public String getCacheKey() {
return "LABEL_CACHE_KEY";
}
}
label-search.zul – Label Search Page in ZUL
The search page with a label name Textfield
to search by label name. The Listbox
will display the data with pagination.
<zk>
<window id="labelSearchWin" zclass="none" border="none" visible="true"
apply="org.kodejava.zk.controller.LabelSearchController" width="100%">
<grid id="searchForm">
<columns>
<column width="200px"/>
<column/>
</columns>
<rows>
<row>
<label value="Label Name"/>
<textbox id="labelNameTextbox" width="450px" maxlength="50"/>
</row>
</rows>
</grid>
<separator/>
<hlayout>
<button id="searchButton" label="Search"/>
</hlayout>
<separator/>
<vlayout>
<listbox id="listbox" mold="paging" pageSize="${labelSearchWin$composer.pageSize}" visible="false">
<listhead>
<listheader label="No" hflex="min"/>
<listheader label="Label Name" sort="auto(name)"/>
<listheader label="Action" hflex="min"/>
</listhead>
<template name="model">
<listitem>
<listcell label="${forEachStatus.index + 1}" hflex="min"/>
<listcell label="${each.name}"/>
<listcell hflex="min">
<hlayout>
<button label="Edit" forward="onClick=listbox.onEdit(${each})" tooltiptext="Edit Data"/>
</hlayout>
</listcell>
</listitem>
</template>
</listbox>
</vlayout>
</window>
</zk>
Running the application and access it at localhost:8080
will give you a screen like the screenshot at the beginning of this post.
The complete source code can be found in the following GitHub repository kodejava-zk-search.