How do I build simple search page using ZK and Spring Boot?

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.

Wayan

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.