Introduction to the Apache POI library for creating Excel file

Apache POI is a robust open-source library that provides APIs for manipulating various file formats based upon the Office Open XML standards (OOXML) and Microsoft’s OLE2 compound document format (OLE2). Being Java-based, it’s readily used by Java developers for creating, reading, and modifying Excel files.

To create a new Excel file using Apache POI, you will want to follow these basic steps:

1. Create a Workbook

You create an instance of Workbook which represents an Excel file. Workbook is an interface and the instance can be created by using one of its implementing classes like HSSFWorkbook (for .xls) or XSSFWorkbook (for .xlsx).

package org.kodejava.poi;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class CreateWorkbook {
   public static void main(String[] args) {
      // Create Blank workbook
      try (XSSFWorkbook workbook = new XSSFWorkbook()) {
         // Write the workbook to a file
         try (FileOutputStream fileOut = new FileOutputStream("workbook.xlsx")) {
            workbook.write(fileOut);
         } catch (Exception e) {
            e.printStackTrace();
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

This block of code will create a new Excel file called “workbook.xlsx” in your project directory.

2. Create a Sheet

You create a Sheet in the Workbook. A sheet is a collection of rows where your data resides. To create a new sheet using Apache POI, you can use the createSheet() method of the Workbook interface. This method returns an object of the Sheet interface.

Below is an example of creating a new sheet in an Excel workbook:

package org.kodejava.poi;

import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class CreateSheetExample {
    public static void main(String[] args) {
        // Create a workbook
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            // Create a blank sheet within the workbook
            XSSFSheet sheet = workbook.createSheet("New Sheet");

            // Write the workbook in a file system
            try (FileOutputStream out = new FileOutputStream("New.xlsx")) {
                workbook.write(out);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This code will create a new sheet named “New Sheet” in an Excel file named New.xlsx.

3. Create Rows and Cells

Inside your Sheet, you create Row. Inside your Row, you create Cell.

Creating new rows and cells with Apache POI is quite straightforward. Here’s a step-by-step way to do this:

  • Create a workbook.
  • Within the workbook, create a sheet.
  • Within the sheet, create a row.
  • Within the row, create a cell.

Each Cell can then be populated with data. Apache POI supports various data types including a text, numeric, date, boolean and formula.

Here is an example using Apache POI in Java:

package org.kodejava.poi;

import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class CreateCellExample {
    public static void main(String[] args) {
        // Create a workbook
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            // Create a blank sheet within the workbook
            XSSFSheet sheet = workbook.createSheet("New Sheet");

            // Create a row within the sheet
            XSSFRow row = sheet.createRow(0);

            // Create a cell within the row
            XSSFCell cell = row.createCell(0);

            // Set the value of the cell
            cell.setCellValue("Hello, Apache POI!");

            // Write the workbook to the file system
            try (FileOutputStream out = new FileOutputStream("Cell.xlsx")) {
                workbook.write(out);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This code will create a new workbook with a sheet named New Sheet. In that sheet, it will create a row at index 0. In that row, it will create a cell at index 0 and set its value to “Hello, Apache POI!”

This will all be saved to a new Excel file named Cell.xlsx in the current project directory.

4. Creating a Date cell

Using Apache POI involves a few steps outlined below:

  • First, you create a new Workbook, Sheet, and Row, as shown previously.
  • Then, you create a cell in any given row and set its type to CellType.NUMERIC.
  • You then use CellStyle to format the date according to your needs.

Here’s an example:

package org.kodejava.poi;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;

public class CreateDateCellExample {
    public static void main(String[] args) {
        // Create a workbook
        try (Workbook workbook = new XSSFWorkbook()) {
            // Create a blank sheet within the workbook
            Sheet sheet = workbook.createSheet("Date Example");

            // Create a row within the sheet
            Row row = sheet.createRow(0);

            // Create a cell within the row and set its type to NUMERIC
            Cell cell = row.createCell(0, CellType.NUMERIC);

            // Create a CellStyle
            CellStyle cellStyle = workbook.createCellStyle();
            CreationHelper creationHelper = workbook.getCreationHelper();
            // Set the built-in date format style
            cellStyle.setDataFormat(creationHelper.createDataFormat().getFormat("m/d/yy h:mm"));

            cell.setCellStyle(cellStyle);

            // Set the value of the cell as today's date
            cell.setCellValue(new Date());

            // Write the workbook to the file system
            try (FileOutputStream out = new FileOutputStream("DateExample.xlsx")) {
                workbook.write(out);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

In this example, a date cell has been created, and the date format has been set to m/d/yy h:mm, but there are many other ways to format the date or time. You can use a custom format if you prefer, so long as it follows the rule of Excel custom date format.

5. Creating various types of cell

Using Apache POI, you need to create a cell and set the cell type and value accordingly.

Let’s take a look at some examples:

package org.kodejava.poi;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class CreateVariousCellsExample {
    public static void main(String[] args) throws IOException {
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("New Sheet");

        Row row = sheet.createRow(0);

        // Create a numeric cell
        Cell numericCell = row.createCell(0, CellType.NUMERIC);
        numericCell.setCellValue(123.456);

        // Create a string cell
        Cell stringCell = row.createCell(1, CellType.STRING);
        stringCell.setCellValue("Hello, POI!");

        // Create a boolean cell
        Cell booleanCell = row.createCell(2, CellType.BOOLEAN);
        booleanCell.setCellValue(true);

        // Create a formula cell
        Cell formulaCell = row.createCell(3, CellType.FORMULA);
        formulaCell.setCellFormula("A1*B1");

        try (FileOutputStream fileOut = new FileOutputStream("./differentCellTypes.xlsx")) {
            workbook.write(fileOut);
        }
        workbook.close();
    }
}

This code will create an Excel file named “differentCellTypes.xlsx” with a sheet named “New Sheet”. In the first row of this sheet, there will be four cells:

  • A numeric cell containing the number 123.456
  • A string cell containing the value “Hello, POI!”
  • A boolean cell containing the boolean value of true
  • A formula cell containing a formula which multiplies the values of the first and second cells.

And that’s the basic of creating an Excel file using Apache POI. More advanced features include formatting and styling cells, adding images, creating charts, formulas, hyperlinks and so forth. You would find Apache POI’s API and its official documentation very resourceful for covering those.

Do note, it’s important to always close the Workbook instance at the end to free up resources and to prevent memory leaks.

Maven Dependencies

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

Maven Central

How do I create XLSX files using Apache POI?

First of all, make sure that you have the Apache POI library included in your project. If you are using Maven, you can add the following to your pom.xml:

Maven Dependencies

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

Maven Central

Then, you can use the following code to create an Excel .xlsx file:

package org.kodejava.poi;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class ExcelExample {
    public static void main(String[] args) {
        // Create a new Workbook
        Workbook workbook = new XSSFWorkbook();

        // Create a new sheet in the workbook with a name
        Sheet sheet = workbook.createSheet("MySheet");

        // Create a row at index 0
        Row row = sheet.createRow(0);

        // Create a cell at index 0
        Cell cell = row.createCell(0, CellType.STRING);
        // Assign the string "Hello" to the cell
        cell.setCellValue("Hello");

        // Add some other rows and cells similarly...
        // ...

        try (FileOutputStream fos = new FileOutputStream("MyExcel.xlsx")) {
            workbook.write(fos);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("Excel file has been generated!");

        try {
            workbook.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This program will create an Excel .xlsx file named MyExcel.xlsx with a single sheet called “MySheet.” The sheet has a single cell in the first row which contains the string “Hello.” The output file will be generated in the root directory of your project.

The above-given code is only a basic example. Apache POI allows advanced features like applying styles to cells, merging cells, and working with formulas among others. The library also supports other Microsoft Office file formats.

Here is another slightly more complex example where we create multiple rows and cells:

package org.kodejava.poi;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class ExcelAnotherExample {
    public static void main(String[] args) {
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Students");

        Row rowHeader = sheet.createRow(0);
        Cell cellHeader1 = rowHeader.createCell(0, CellType.STRING);
        cellHeader1.setCellValue("ID");

        Cell cellHeader2 = rowHeader.createCell(1, CellType.STRING);
        cellHeader2.setCellValue("Name");

        for(int i = 1; i < 6; i++) {
            Row rowData = sheet.createRow(i);

            Cell cellData1 = rowData.createCell(0, CellType.NUMERIC);
            cellData1.setCellValue(i);

            Cell cellData2 = rowData.createCell(1, CellType.STRING);
            cellData2.setCellValue("Student " + i);
        }

        try (FileOutputStream fos = new FileOutputStream("StudentsData.xlsx")) {
            workbook.write(fos);
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("Excel file has been generated!");

        try {
            workbook.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This code creates an Excel file named StudentsData.xlsx with a single worksheet. The worksheet contains a listing of student names and their respective IDs. The first row is a header with “ID” and “Name” labels. The next rows contain the IDs (numeric) and names (string “Student X”) for five different students.

How do I split large excel file into multiple smaller files?

A friend of mine told me that he has a large Excel file, and he asked me if I could split the file into multiple smaller Excel files. So I write this little program using Apache POI to do it.

The code snippet below basically contains the following steps:

  1. Load the Excel as an InputStream from the classpath, if the file was not found the program will exit.
  2. Create XSSFWorkbook from the input stream, and get the first XSSFSheet from the workbook.
  3. Iterate the rows of data from the source worksheet.
  4. On the first rownum for each split file we create a new workbook using SXSSFWorkbook and also create the SXSSFSheet.
  5. Read the first row from the source worksheet, store it in headerRow to be used for creating header row in each new sheet.
  6. If we are at the first row, write the header.
  7. Write each row from the source sheet to the destination sheet untuk the max rows is reached.
  8. Write the workbook into a new file.

And here is the complete code that you can try.

package org.kodejava.poi;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class SplitExcelDemo {
    public static final int MAX_ROWS_PER_FILE = 1000 - 1;
    public static final boolean WITH_HEADER = true;

    public static void main(String[] args) {
        LocalDateTime startTime = LocalDateTime.now();
        String filename = "/stock.xlsx";
        try (InputStream is = SplitExcelDemo.class.getResourceAsStream(filename)) {
            if (is == null) {
                System.out.println("Source file was not found!");
                System.exit(1);
            }

            XSSFWorkbook srcWorkbook = new XSSFWorkbook(is);
            XSSFSheet srcSheet = srcWorkbook.getSheetAt(0);
            int physicalNumberOfRows = srcSheet.getPhysicalNumberOfRows();

            int rownum = 0;
            int splitCounter = 0;

            SXSSFWorkbook destWorkbook = null;
            SXSSFSheet destSheet = null;

            XSSFRow headerRow = null;

            for (Row srcRow : srcSheet) {
                if (rownum == 0) {
                    // At the beginning let's create a new workbook and worksheet
                    destWorkbook = new SXSSFWorkbook();
                    destSheet = destWorkbook.createSheet();
                }

                if (srcRow.getRowNum() == 0 && WITH_HEADER) {
                    // Copy header row to be use in each split file
                    headerRow = (XSSFRow) srcRow;
                }

                if (rownum == 0 && WITH_HEADER) {
                    // Add row header to each split file
                    if (headerRow != null) {
                        SXSSFRow firstRow = destSheet.createRow(rownum);
                        int index = 0;
                        for (Cell cell : headerRow) {
                            SXSSFCell headerCell = firstRow.createCell(index++, cell.getCellType());
                            if (cell.getCellType() == CellType.STRING) {
                                headerCell.setCellValue(cell.getStringCellValue());
                            }
                        }
                    }
                } else {
                    // Copy rows from source worksheet into destination worksheet
                    SXSSFRow descRow = destSheet.createRow(rownum);
                    int index = 0;
                    for (Cell cell : srcRow) {
                        SXSSFCell destCell = descRow.createCell(index++, cell.getCellType());
                        switch (cell.getCellType()) {
                            case NUMERIC -> destCell.setCellValue(cell.getNumericCellValue());
                            case STRING -> destCell.setCellValue(cell.getStringCellValue());
                        }
                    }
                }

                // When a max number of rows copied are reached, or when we are at the end of worksheet, 
                // write data into a new file 
                if (rownum == MAX_ROWS_PER_FILE || srcRow.getRowNum() == physicalNumberOfRows - 1) {
                    rownum = -1;
                    String output = String.format("split-%03d.xlsx", splitCounter++);
                    System.out.println("Writing " + output);
                    try (OutputStream os = new FileOutputStream(output)) {
                        destWorkbook.write(os);
                    } catch (IOException e){
                        e.printStackTrace();
                    }
                }

                rownum = rownum + 1;
            }

            // Display processing time
            LocalDateTime endTime = LocalDateTime.now();
            long minutes = startTime.until(endTime, ChronoUnit.MINUTES);
            startTime = startTime.plusMinutes(minutes);
            long seconds = startTime.until(endTime, ChronoUnit.SECONDS);
            System.out.printf("Splitting finished in %d minutes and %d seconds %n", minutes, seconds);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The output of running this program will look like this:

Writing split-000.xlsx
Writing split-001.xlsx
Writing split-002.xlsx
Writing split-003.xlsx
Writing split-004.xlsx
Writing split-005.xlsx
Writing split-006.xlsx
Writing split-007.xlsx
Writing split-008.xlsx
Writing split-009.xlsx
Splitting finished in 0 minutes and 8 seconds 

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.5</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.5</version>
    </dependency>
</dependencies>

Maven Central Maven Central

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.

How do I convert Map to JSON and vice versa using Jackson?

In the following code snippet we will convert java.util.Map object into JSON string and convert back the JSON string into Java Map object. In this example we will be using the Jackson library.

To convert from Map to JSON string the steps are:

  • Create a map of string keys and values.
  • Create an instance of Jackson ObjectMapper.
  • To convert map to JSON string we call the writeValueAsString() method and pass the map as argument.
// Converting Map to JSON
String json = null;
try {
    json = mapper.writeValueAsString(colours);
    System.out.println("json = " + json);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

Now, to convert from JSON string back to map we can do it in the following steps:

  • Create a JSON string, in this case we use the one converted from the colours map.
  • Create an instance of Jackson ObjectMapper.
  • Call the mapper’s readValue() method with JSON string and an empty instance of TypeReference as arguments.
// Converting JSON to MAP
try {
    Map<String, String> newColours =
            mapper.readValue(json, new TypeReference<>() {});
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

And here is the complete code snippet.

package org.kodejava.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class MapToJson {
    public static void main(String[] args) {
        Map<String, String> colours = new HashMap<>();
        colours.put("BLACK", "#000000");
        colours.put("RED", "#FF0000");
        colours.put("GREEN", "#008000");
        colours.put("BLUE", "#0000FF");
        colours.put("YELLOW", "#FFFF00");
        colours.put("WHITE", "#FFFFFF");

        ObjectMapper mapper = new ObjectMapper();

        // Converting Map to JSON
        String json = null;
        try {
            json = mapper.writeValueAsString(colours);
            System.out.println("json = " + json);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        // Converting JSON to MAP
        try {
            Map<String, String> newColours =
                    mapper.readValue(json, new TypeReference<>() {});
            System.out.println("Map:");
            for (var entry : newColours.entrySet()) {
                System.out.println(entry.getKey() + " = " + entry.getValue());
            }
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

Running the code snippet above will print the following output:

json = {"RED":"#FF0000","WHITE":"#FFFFFF","BLUE":"#0000FF","BLACK":"#000000","YELLOW":"#FFFF00","GREEN":"#008000"}
Map:
RED = #FF0000
WHITE = #FFFFFF
BLUE = #0000FF
BLACK = #000000
YELLOW = #FFFF00
GREEN = #008000

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.1</version>
    </dependency>
</dependencies>

Maven Central Maven Central