How do I create a string of repeated characters?

The following code demonstrates how to create a string of repeated characters. We use the String.repeat(int count) method introduced in Java 11. This method takes one parameter of type int which is the number of times to repeat the string. The count must be a positive number, a negative number will cause this method to throw java.lang.IllegalArgumentException.

In the snippet below, we use the method to repeat characters and draw some triangles. We combine the repeat() method with a for loop to draw the triangles.

package org.kodejava.basic;

public class StringRepeatDemo {
    public static void main(String[] args) {
        String star = "*";
        String fiveStars = star.repeat(5);
        System.out.println("fiveStars = " + fiveStars);

        String arrow = "-->";
        String arrows = arrow.repeat(10);
        System.out.println("arrows    = " + arrows);

        String asterisk = "#";
        for (int i = 1; i <= 10; i++) {
            System.out.println(asterisk.repeat(i));
        }

        int height = 10;
        for (int i = 1, j = 1; i <= height; i++, j += 2) {
            System.out.println(" ".repeat(height - i) + "*".repeat(j));
        }
    }
}

The outputs of the code snippet above are:

fiveStars = *****
arrows    = -->-->-->-->-->-->-->-->-->-->
#
##
###
####
#####
######
#######
########
#########
##########
         *
        ***
       *****
      *******
     *********
    ***********
   *************
  ***************
 *****************
*******************

How do I convert datetime string with optional part to a date object?

Since JDK 8, we can create a datetime formatter / parser pattern that can have optional sections. When parsing a datetime string that contains optional values, for example, a date without time part or a datetime without second part, we can create a parsing pattern wrapped within the [] symbols. The [ character is the optional section start symbol, and the ] character is the optional section end symbol. The pattern inside this symbol will be considered as an optional value.

We can use the java.time.format.DateTimeFormatter class to parse the string of datetime or format the datetime object, and use it with the new Java time API classes such as java.time.LocalDate or java.time.LocalDateTime to convert the string into respective LocalDate or LocalDateTime object as show in the code snippet below.

package org.kodejava.datetime;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeParseOptionalParts {
    public static final String OPT_TIME_PATTERN = "yyyy-MM-dd[ HH:mm[:ss]]";
    public static final String OPT_SECOND_PATTERN = "yyyy-MM-dd HH:mm[:ss]";

    public static void main(String[] args) {
        DateTimeFormatter optTimeFormatter = DateTimeFormatter.ofPattern(OPT_TIME_PATTERN);
        LocalDate date1 = LocalDate.parse("2023-08-28", optTimeFormatter);
        LocalDate date2 = LocalDate.parse("2023-08-28 17:15", optTimeFormatter);
        LocalDate date3 = LocalDate.parse("2023-08-28 17:15:30", optTimeFormatter);
        System.out.println("date1 = " + date1);
        System.out.println("date2 = " + date2);
        System.out.println("date3 = " + date3);

        DateTimeFormatter optSecondFormatter = DateTimeFormatter.ofPattern(OPT_SECOND_PATTERN);
        LocalDateTime datetime1 = LocalDateTime.parse("2023-08-28 17:15", optSecondFormatter);
        LocalDateTime datetime2 = LocalDateTime.parse("2023-08-28 17:15:30", optSecondFormatter);
        System.out.println("datetime1 = " + datetime1);
        System.out.println("datetime2 = " + datetime2);
    }
}

Here are the outputs of the code snippet above:

date1 = 2023-08-28
date2 = 2023-08-28
date3 = 2023-08-28
datetime1 = 2023-08-28T17:15
datetime2 = 2023-08-28T17:15:30

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.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
</dependencies>

Maven Central Maven Central

How do I get the number of processors available to the JVM?

The Runtime.getRuntime().availableProcessors() method returns the maximum number of processors available to the Java virtual machine, the value will never be smaller than one. Knowing the number of available processor you can use it for example to limit the number of thread in your application when you are writing a multi-thread code.

package org.kodejava.lang;

public class NumberProcessorExample {
    public static void main(String[] args) {
        final int processors = Runtime.getRuntime().availableProcessors();
        System.out.println("Number of processors = " + processors);
    }
}

Running the code snippet give you something like:

Number of processors = 8

How do I show Spring transaction in log / console?

When you use the Spring framework @Transactional annotation in your service layer you might want to see what is happening in your code related to database transaction. You want to see when a transaction is started, when it is committed or rollbacked.

To activate the log for transactional message you can add the following configurations in your application properties file. For example when using the JpaTransactionManager you can set the log level to DEBUG.

logging.level.root=INFO

logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG

Running the spring boot application with these configuration, the JpaTransactionManager will write something line these on your log file or console:

2023-03-29T23:06:52.194+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.kodejava.webapp.accounting.service.impl.CalculationServiceImpl.recalculate]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2023-03-29T23:06:52.194+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(974784570<open>)] for JPA transaction
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@4150907e]
...
...
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(974784570<open>)] for JPA transaction
2023-03-29T23:06:52.195+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
...
...
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(974784570<open>)]
2023-03-29T23:06:52.237+08:00 DEBUG 54056 --- [nio-9090-exec-3] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(974784570<open>)] after transaction

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 calculate days between two dates excluding weekends and holidays?

The code snippet below shows you a simple way to calculate days between two dates excluding weekends and holidays. As an example, you can use this function for calculating work days. The snippet utilize the java.time API and the Stream API to calculate the value.

What we do in the code below can be described as the following:

  • Create a list of holidays. The dates might be read from a database or a file.
  • Define filter Predicate for holidays.
  • Define filter Predicate for weekends.
  • These predicates will be use for filtering the days between two dates.
  • Define the startDate and the endDate to be calculated.
  • Using Stream.iterate() we iterate the dates, filter it based on the defined predicates.
  • Finally, we get the result as list.
  • The actual days between is the size of the list, workDays.size().
package org.kodejava.datetime;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class DaysBetweenDates {
    public static void main(String[] args) {
        List<LocalDate> holidays = new ArrayList<>();
        holidays.add(LocalDate.of(2022, Month.DECEMBER, 26));
        holidays.add(LocalDate.of(2023, Month.JANUARY, 2));

        Predicate<LocalDate> isHoliday = holidays::contains;
        Predicate<LocalDate> isWeekend = date -> date.getDayOfWeek() == DayOfWeek.SATURDAY
                || date.getDayOfWeek() == DayOfWeek.SUNDAY;

        LocalDate startDate = LocalDate.of(2022, Month.DECEMBER, 23);
        LocalDate endDate = LocalDate.of(2023, Month.JANUARY, 3);
        System.out.println("Start date = " + startDate);
        System.out.println("End date   = " + endDate);

        // Days between startDate inclusive and endDate exclusive
        long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
        System.out.println("Days between = " + daysBetween);

        List<LocalDate> workDays = Stream.iterate(startDate, date -> date.plusDays(1))
                .limit(daysBetween)
                .filter(isHoliday.or(isWeekend).negate())
                .toList();

        long actualDaysBetween = workDays.size();
        System.out.println("Actual days between = " + actualDaysBetween);
    }
}

Running the code snippet above give us the following result:

Start date = 2022-12-23
End date   = 2023-01-03
Days between = 11
Actual days between = 5

How do I discover the quarter of a given date?

The following code snippet shows you a various way to get the quarter of a given date. Some methods that we use below are:

  • Using the new java.time API of Java 8 IsoFields.QUARTER_OF_YEAR.
  • Using Java 8 DateTimeFormatter pattern of Q or q. The length of “q” give us a different result.
  • Using java.util.Date.
  • Using java.util.Calendar.
  • Get the quarter from an array of string.

Let’s see the code snippet in action.

package org.kodejava.datetime;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.IsoFields;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

public class DateQuarter {
    public static void main(String[] args) {
        // Using Java 8
        LocalDate now = LocalDate.now();
        int quarter = now.get(IsoFields.QUARTER_OF_YEAR);
        System.out.println("quarter  = " + quarter);

        // Using DateTimeFormatter Q / q, set the Locale to get value
        // in local format
        String quarter1 = LocalDate.of(2023, 8, 17)
                .format(DateTimeFormatter.ofPattern("q", Locale.US));
        String quarter2 = LocalDate.of(2023, 8, 17)
                .format(DateTimeFormatter.ofPattern("qq", Locale.US));
        String quarter3 = LocalDate.of(2023, 8, 17)
                .format(DateTimeFormatter.ofPattern("qqq", Locale.US));
        String quarter4 = LocalDate.of(2023, 8, 17)
                .format(DateTimeFormatter.ofPattern("qqqq", Locale.US));
        System.out.println("quarter1 = " + quarter1);
        System.out.println("quarter2 = " + quarter2);
        System.out.println("quarter3 = " + quarter3);
        System.out.println("quarter4 = " + quarter4);

        // Using older version of Java
        Date today = new Date();
        quarter = (today.getMonth() / 3) + 1;
        System.out.println("quarter = " + quarter);

        // Using java.util.Calendar object. For certain date
        // we can set the calendar date using setTime() method.
        Calendar calendar = Calendar.getInstance();
        quarter = (calendar.get(Calendar.MONTH) / 3) + 1;
        System.out.println("quarter = " + quarter);

        // Custom the quarter as text
        String[] quarters = new String[]{"Q1", "Q2", "Q3", "Q4"};
        String quarterString = quarters[quarter - 1];
        System.out.println("quarterString = " + quarterString);
    }
}

And here are the result of the code snippet above:

quarter  = 1
quarter1 = 3
quarter2 = 03
quarter3 = Q3
quarter4 = 3rd quarter
quarter = 1
quarter = 1
quarterString = Q1

How do I export MySQL database schema into markdown format?

The following code example demonstrate how to export MySQL database schema into markdown table format. We get the table structure information by executing MySQL’s DESCRIBE statement.

The steps we do in the code snippet below:

  • Connect to the database.
  • We obtain the list of table name from the database / schema.
  • Executes DESCRIBE statement for each table name.
  • Read table structure information such as field, type, null, key, default and extra.
  • Write the information into markdown table format and save it into table.md.

And here are the complete code snippet.

package org.kodejava.jdbc;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class DescribeMySQLToMarkDown {
    private static final String URL = "jdbc:mysql://localhost/kodejava";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "";

    public static void main(String[] args) {
        String tableQuery = """
                select table_name
                from information_schema.tables
                where table_schema = 'kodejava'
                  and table_type = 'BASE TABLE'
                order by table_name;
                """;

        try (Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD)) {
            Statement stmt = connection.createStatement();
            ResultSet resultSet = stmt.executeQuery(tableQuery);
            List<String> tables = new ArrayList<>();
            while (resultSet.next()) {
                tables.add(resultSet.getString("table_name"));
            }

            System.out.println(tables.size() + " tables found.");

            try (BufferedWriter writer = new BufferedWriter(new FileWriter("table.md"))) {
                for (String table : tables) {
                    System.out.println("Processing table: " + table);
                    Statement statement = connection.createStatement();
                    ResultSet descResult = statement.executeQuery("DESCRIBE " + table);

                    writer.write(String.format("Table Name: **%s**%n%n", table));
                    writer.write("| Field Name | Data Type | Null | Key | Default | Extra |\n");
                    writer.write("|:---|:---|:---|:---|:---|:---|\n");
                    while (descResult.next()) {
                        String field = descResult.getString("field");
                        String type = descResult.getString("type");
                        String nullInfo = descResult.getString("null");
                        String key = descResult.getString("key");
                        String defaultInfo = descResult.getString("default");
                        String extra = descResult.getString("extra");
                        String line = String.format("| %s | %s | %s | %s | %s | %s |%n",
                                field, type, nullInfo, key, defaultInfo, extra);
                        writer.write(line);
                    }
                    writer.write("\n<br/>\n<br/>\n");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

This code snippet will produce something like below. I have tidy up the markdown for a better presentation.

Table Name: **books**

| Field Name | Data Type       | Null | Key | Default | Extra          |
|:-----------|:----------------|:-----|:----|:--------|:---------------|
| id         | bigint unsigned | NO   | PRI | null    | auto_increment |
| isbn       | varchar(30)     | NO   |     | null    |                |

<br/>
<br/>

Maven dependencies

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.1.0</version>
</dependency>

Maven Central

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.15.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

Maven Central Maven Central