This is an automated email from the ASF dual-hosted git repository.

pefernan pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/incubator-kie-kogito-runtimes.git


The following commit(s) were added to refs/heads/main by this push:
     new 8dde7b8ff5 [incubator-kie-issues#1612] Fix BusinessCalendar behavior 
with inconsistent (full/partial) set of properties on `calendar.properties` 
(#3788)
8dde7b8ff5 is described below

commit 8dde7b8ff5537c850519d21435ae5abfdfcf1186
Author: Abhiram Gundala <[email protected]>
AuthorDate: Mon Dec 9 06:30:05 2024 -0500

    [incubator-kie-issues#1612] Fix BusinessCalendar behavior with inconsistent 
(full/partial) set of properties on `calendar.properties` (#3788)
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] TODO setup
    
    * [incubator-kie-issues#1612] Separating different concerns in different 
classes: CalendarBean is responsible of calendar.properties file. 
BusinessCalendarImpl is responsible of working time evaluation
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Example test
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Extend test coverage. Minor refactoring 
related to it.
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Comment on tests
    
    * [incubator-kie-issues#1612] Minor fixes
    
    * [incubator-kie-issues#1612] Extend test coverage. Minor refactoring 
related to it.
    
    * [incubator-kie-issues#1612] Minor refactoring
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Fixing tests. Including 
incubator-kie-issues#1648 fix
    
    * [incubator-kie-issues#1612] WIP - implementing nightly hour
    
    * [incubator-kie-issues#1612] WIP - Simplify test - moving tested methods 
to static
    
    * [incubator-kie-issues#1612] WIP - Cleanup
    
    * [incubator-kie-issues#1612] Working tests.
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Fixed logging
    
    * [incubator-kie-issues#1612] Fixed minute/second reset on calendarRolling
    
    * [incubator-kie-issues#1612] Fixed assertiont JUnit5 -> assertj
    
    * [incubator-kie-issues#1612] Fixed test
    
    * incubator-kie-issues-1612
    
    * incubator-kie-issues-1612
    
    * [incubator-kie-issues#1612] Avoid minute/second reset on rolling hour, 
since the minute/second management is based on "add" operation
    
    * [incubator-kie-issues#1612] Fix naming
    
    * [incubator-kie-issues#1612] Add minute / second test/fix
    
    * [incubator-kie-issues#1612] Extend test coverage
    
    * updated logging
    
    * logger update
    
    * logger update
    
    ---------
    
    Co-authored-by: Gabriele-Cardosi <[email protected]>
---
 .../org/kie/kogito/calendar/BusinessCalendar.java  |  13 +-
 .../process/core/timer/BusinessCalendarImpl.java   | 622 +++++++++---------
 .../org/jbpm/process/core/timer/CalendarBean.java  | 399 ++++++++++++
 .../process/core/timer/CalendarBeanFactory.java    |  60 ++
 .../core/timer/BusinessCalendarImplTest.java       | 696 +++++++++------------
 .../core/timer/CalendarBeanFactoryTest.java        |  45 ++
 .../jbpm/process/core/timer/CalendarBeanTest.java  | 297 +++++++++
 .../src/test/resources/calendar.properties         |  20 +
 jbpm/jbpm-flow/src/test/resources/logback-test.xml |   1 +
 ....java => BusinessCalendarTimerProcessTest.java} |  24 +-
 .../BusinessCalendarProducerQuarkusTemplate.java   |   2 +-
 .../BusinessCalendarProducerSpringTemplate.java    |   2 +-
 .../BusinessCalendarProducerQuarkusTemplate.java   |  36 --
 .../BusinessCalendarProducerSpringTemplate.java    |  37 --
 14 files changed, 1450 insertions(+), 804 deletions(-)

diff --git 
a/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java 
b/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java
index 6e15a6cc0e..45ecce21bd 100644
--- a/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java
+++ b/api/kogito-api/src/main/java/org/kie/kogito/calendar/BusinessCalendar.java
@@ -27,18 +27,21 @@ import java.util.Date;
 public interface BusinessCalendar {
 
     /**
-     * Calculates given time expression into duration in milliseconds based on 
calendar configuration.
-     * 
+     * Returns the difference, in milliseconds, between the <b>business 
date</b> that matches the given
+     * <code>timeExpression</code>, and the current time.
+     * See {@link #calculateBusinessTimeAsDate} for <b>business date</b> 
calculation
+     *
      * @param timeExpression time expression that is supported by business 
calendar implementation.
      * @return duration expressed in milliseconds
      */
-    public long calculateBusinessTimeAsDuration(String timeExpression);
+    long calculateBusinessTimeAsDuration(String timeExpression);
 
     /**
-     * Calculates given time expression into target date based on calendar 
configuration.
+     * Returns the first <code>Date</code> that matches the given 
<code>timeExpression</code> and falls
+     * into the business calendar working hours.
      * 
      * @param timeExpression time expression that is supported by business 
calendar implementation.
      * @return date when given time expression will match in the future
      */
-    public Date calculateBusinessTimeAsDate(String timeExpression);
+    Date calculateBusinessTimeAsDate(String timeExpression);
 }
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java
 
b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java
index b22f48cbb3..280cc3f6e3 100755
--- 
a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java
+++ 
b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/BusinessCalendarImpl.java
@@ -18,48 +18,37 @@
  */
 package org.jbpm.process.core.timer;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.text.SimpleDateFormat;
 import java.time.Duration;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.Objects;
-import java.util.Properties;
 import java.util.TimeZone;
 import java.util.regex.Matcher;
 
 import org.jbpm.util.PatternConstants;
 import org.kie.kogito.calendar.BusinessCalendar;
-import org.kie.kogito.timer.SessionClock;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static 
org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_PATH;
-
 /**
  * Default implementation of BusinessCalendar interface that is configured 
with properties.
  * Following are supported properties:
  * <ul>
- * <li>business.days.per.week - specifies number of working days per week 
(default 5)</li>
- * <li>business.hours.per.day - specifies number of working hours per day 
(default 8)</li>
- * <li>business.start.hour - specifies starting hour of work day (default 
9AM)</li>
- * <li>business.end.hour - specifies ending hour of work day (default 5PM)</li>
+ * <li>business.start.hour - specifies starting hour of work day 
(mandatory)</li>
+ * <li>business.end.hour - specifies ending hour of work day (mandatory)</li>
  * <li>business.holidays - specifies holidays (see format section for details 
on how to configure it)</li>
  * <li>business.holiday.date.format - specifies holiday date format used 
(default yyyy-MM-dd)</li>
- * <li>business.weekend.days - specifies days of the weekend (default Saturday 
and Sunday)</li>
+ * <li>business.weekend.days - specifies days of the weekend (default Saturday 
(7) and Sunday (1), use 0 to indicate no weekend days)</li>
  * <li>business.cal.timezone - specifies time zone to be used (if not given 
uses default of the system it runs on)</li>
  * </ul>
- * 
+ *
  * <b>Format</b><br/>
- * 
+ *
  * Holidays can be given in two formats:
  * <ul>
  * <li>as date range separated with colon - for instance 
2012-05-01:2012-05-15</li>
@@ -68,28 +57,29 @@ import static 
org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDA
  * each holiday period should be separated from next one with comma: 
2012-05-01:2012-05-15,2012-12-24:2012-12-27
  * <br/>
  * Holiday date format must be given in pattern that is supported by 
<code>java.text.SimpleDateFormat</code>.<br/>
- * 
- * Weekend days should be given as integer that corresponds to 
<code>java.util.Calendar</code> constants.
+ *
+ * Weekend days should be given as integer that corresponds to 
<code>java.util.Calendar</code> constants, use 0 to indicate no weekend days
  * <br/>
- * 
  */
 public class BusinessCalendarImpl implements BusinessCalendar {
 
     private static final Logger logger = 
LoggerFactory.getLogger(BusinessCalendarImpl.class);
 
-    private Properties businessCalendarConfiguration;
-
     private static final long HOUR_IN_MILLIS = 60 * 60 * 1000;
 
-    private int daysPerWeek;
-    private int hoursInDay;
-    private int startHour;
-    private int endHour;
-    private String timezone;
+    private final int daysPerWeek;
+    private final int hoursInDay;
+    private final int startHour;
+    private final int endHour;
+    private final String timezone;
 
-    private List<TimePeriod> holidays;
-    private List<Integer> weekendDays = new ArrayList<>();
-    private SessionClock clock;
+    private final List<TimePeriod> holidays;
+    private final List<Integer> weekendDays;
+
+    /**
+     * Testing calendar used only for testing purposes
+     */
+    private final Calendar testingCalendar;
 
     private static final int SIM_WEEK = 3;
     private static final int SIM_DAY = 5;
@@ -97,8 +87,6 @@ public class BusinessCalendarImpl implements BusinessCalendar 
{
     private static final int SIM_MIN = 9;
     private static final int SIM_SEC = 11;
 
-    public static final String DAYS_PER_WEEK = "business.days.per.week";
-    public static final String HOURS_PER_DAY = "business.hours.per.day";
     public static final String START_HOUR = "business.start.hour";
     public static final String END_HOUR = "business.end.hour";
     // holidays are given as date range and can have more than one value 
separated with comma
@@ -108,97 +96,53 @@ public class BusinessCalendarImpl implements 
BusinessCalendar {
     public static final String WEEKEND_DAYS = "business.weekend.days";
     public static final String TIMEZONE = "business.cal.timezone";
 
-    public BusinessCalendarImpl() {
-        this(null);
+    public static Builder builder() {
+        return new Builder();
     }
 
-    public BusinessCalendarImpl(Properties configuration) {
-        this(configuration, null);
+    /**
+     *
+     * @param testingCalendar is used only for testing purpose. It is 
<code>null</code> in production and
+     *        during normal execution
+     */
+    private BusinessCalendarImpl(Calendar testingCalendar) {
+        this(CalendarBeanFactory.createCalendarBean(), testingCalendar);
     }
 
-    public BusinessCalendarImpl(Properties configuration, SessionClock clock) {
-        this.clock = clock;
-        if (configuration == null) {
-            businessCalendarConfiguration = new Properties();
-            URL resource = 
Thread.currentThread().getContextClassLoader().getResource(BUSINESS_CALENDAR_PATH);
-            if (Objects.nonNull(resource)) {
-                try (InputStream is = resource.openStream()) {
-                    businessCalendarConfiguration.load(is);
-                } catch (IOException e) {
-                    logger.error("Error while loading properties for business 
calendar", e);
-                    throw new RuntimeException("Error while loading properties 
for business calendar", e);
-                }
-            }
-
-        } else {
-            this.businessCalendarConfiguration = configuration;
-        }
-        init();
-    }
-
-    protected void init() {
-        daysPerWeek = getPropertyAsInt(DAYS_PER_WEEK, "5");
-        hoursInDay = getPropertyAsInt(HOURS_PER_DAY, "8");
-        startHour = getPropertyAsInt(START_HOUR, "9");
-        endHour = getPropertyAsInt(END_HOUR, "17");
-        holidays = parseHolidays();
-        parseWeekendDays();
-        this.timezone = businessCalendarConfiguration.getProperty(TIMEZONE);
-    }
-
-    protected String adoptISOFormat(String timeExpression) {
-
-        try {
-            Duration p = null;
-            if (DateTimeUtils.isPeriod(timeExpression)) {
-                p = Duration.parse(timeExpression);
-            } else if (DateTimeUtils.isNumeric(timeExpression)) {
-                p = Duration.of(Long.valueOf(timeExpression), 
ChronoUnit.MILLIS);
-            } else {
-                OffsetDateTime dateTime = OffsetDateTime.parse(timeExpression, 
DateTimeFormatter.ISO_DATE_TIME);
-                p = Duration.between(OffsetDateTime.now(), dateTime);
-            }
-
-            long days = p.toDays();
-            long hours = p.toHours() % 24;
-            long minutes = p.toMinutes() % 60;
-            long seconds = p.getSeconds() % 60;
-            long milis = p.toMillis() % 1000;
-
-            StringBuffer time = new StringBuffer();
-            if (days > 0) {
-                time.append(days + "d");
-            }
-            if (hours > 0) {
-                time.append(hours + "h");
-            }
-            if (minutes > 0) {
-                time.append(minutes + "m");
-            }
-            if (seconds > 0) {
-                time.append(seconds + "s");
-            }
-            if (milis > 0) {
-                time.append(milis + "ms");
-            }
-
-            return time.toString();
-        } catch (Exception e) {
-            return timeExpression;
-        }
+    private BusinessCalendarImpl(CalendarBean calendarBean, Calendar 
testingCalendar) {
+        holidays = calendarBean.getHolidays();
+        weekendDays = calendarBean.getWeekendDays();
+        daysPerWeek = calendarBean.getDaysPerWeek();
+        timezone = calendarBean.getTimezone();
+        startHour = calendarBean.getStartHour();
+        endHour = calendarBean.getEndHour();
+        hoursInDay = calendarBean.getHoursInDay();
+        this.testingCalendar = testingCalendar;
+        logger.debug("\tholidays: {},\n\tweekendDays: {},\n\tdaysPerWeek: 
{},\n\ttimezone: {},\n\tstartHour: {},\n\tendHour: {},\n\thoursInDay: {}",
+                holidays, weekendDays, daysPerWeek, timezone, startHour, 
endHour, hoursInDay);
     }
 
+    /**
+     * @inheritDoc
+     */
+    @Override
     public long calculateBusinessTimeAsDuration(String timeExpression) {
+        logger.trace("timeExpression {}", timeExpression);
         timeExpression = adoptISOFormat(timeExpression);
 
         Date calculatedDate = calculateBusinessTimeAsDate(timeExpression);
+        logger.debug("calculatedDate: {}, currentTime: {}, timeExpression: {}, 
Difference: {} ms",
+                calculatedDate, new Date(getCurrentTime()), timeExpression, 
calculatedDate.getTime() - getCurrentTime());
 
-        long calculatedDurationInMs = (calculatedDate.getTime() - 
getCurrentTime());
-        logger.debug("calculateBusinessTimeAsDuration for expression {} 
returns {} seconds", timeExpression, (calculatedDurationInMs / 1000));
-        return calculatedDurationInMs;
+        return (calculatedDate.getTime() - getCurrentTime());
     }
 
+    /**
+     * @inheritDoc
+     */
+    @Override
     public Date calculateBusinessTimeAsDate(String timeExpression) {
+        logger.trace("timeExpression {}", timeExpression);
         timeExpression = adoptISOFormat(timeExpression);
 
         String trimmed = timeExpression.trim();
@@ -208,7 +152,7 @@ public class BusinessCalendarImpl implements 
BusinessCalendar {
         int min = 0;
         int sec = 0;
 
-        if (trimmed.length() > 0) {
+        if (!trimmed.isEmpty()) {
             Matcher mat = 
PatternConstants.SIMPLE_TIME_DATE_MATCHER.matcher(trimmed);
             if (mat.matches()) {
                 weeks = (mat.group(SIM_WEEK) != null) ? 
Integer.parseInt(mat.group(SIM_WEEK)) : 0;
@@ -218,121 +162,208 @@ public class BusinessCalendarImpl implements 
BusinessCalendar {
                 sec = (mat.group(SIM_SEC) != null) ? 
Integer.parseInt(mat.group(SIM_SEC)) : 0;
             }
         }
+        logger.trace("weeks: {}", weeks);
+        logger.trace("days: {}", days);
+        logger.trace("hours: {}", hours);
+        logger.trace("min: {}", min);
+        logger.trace("sec: {}", sec);
         int time = 0;
 
-        Calendar c = new GregorianCalendar();
+        Calendar calendar = getCalendar();
+        logger.trace("calendar selected for business calendar: {}", 
calendar.getTime());
         if (timezone != null) {
-            c.setTimeZone(TimeZone.getTimeZone(timezone));
-        }
-        if (this.clock != null) {
-            c.setTimeInMillis(this.clock.getCurrentTime());
+            calendar.setTimeZone(TimeZone.getTimeZone(timezone));
         }
 
         // calculate number of weeks
         int numberOfWeeks = days / daysPerWeek + weeks;
+        logger.trace("number of weeks: {}", numberOfWeeks);
         if (numberOfWeeks > 0) {
-            c.add(Calendar.WEEK_OF_YEAR, numberOfWeeks);
+            calendar.add(Calendar.WEEK_OF_YEAR, numberOfWeeks);
         }
-        handleWeekend(c, hours > 0 || min > 0);
+        logger.trace("calendar WEEK_OF_YEAR: {}", 
calendar.get(Calendar.WEEK_OF_YEAR));
+        rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar, 
weekendDays, hours > 0 || min > 0);
         hours += (days - (numberOfWeeks * daysPerWeek)) * hoursInDay;
 
         // calculate number of days
         int numberOfDays = hours / hoursInDay;
+        logger.trace("numberOfDays: {}", numberOfDays);
         if (numberOfDays > 0) {
             for (int i = 0; i < numberOfDays; i++) {
-                c.add(Calendar.DAY_OF_YEAR, 1);
-                handleWeekend(c, false);
-                handleHoliday(c, hours > 0 || min > 0);
+                calendar.add(Calendar.DAY_OF_YEAR, 1);
+                boolean resetTime = false;
+                rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar, 
weekendDays, resetTime);
+                logger.trace("calendar after rolling to next working day: {} 
when number of days > 0", calendar.getTime());
+                rollCalendarAfterHolidays(calendar, holidays, weekendDays, 
hours > 0 || min > 0);
+                logger.trace("calendar after holidays when number of days > 0: 
{}", calendar.getTime());
             }
         }
-
-        int currentCalHour = c.get(Calendar.HOUR_OF_DAY);
-        if (currentCalHour >= endHour) {
-            c.add(Calendar.DAY_OF_YEAR, 1);
-            c.add(Calendar.HOUR_OF_DAY, startHour - currentCalHour);
-            c.set(Calendar.MINUTE, 0);
-            c.set(Calendar.SECOND, 0);
-        } else if (currentCalHour < startHour) {
-            c.add(Calendar.HOUR_OF_DAY, startHour - currentCalHour);
-            c.set(Calendar.MINUTE, 0);
-            c.set(Calendar.SECOND, 0);
-        }
+        int currentCalHour = calendar.get(Calendar.HOUR_OF_DAY);
+        boolean resetMinuteSecond = currentCalHour >= endHour || 
currentCalHour < startHour;
+        rollCalendarToWorkingHour(calendar, resetMinuteSecond);
+        logger.trace("calendar after rolling to working hour: {}", 
calendar.getTime());
 
         // calculate remaining hours
         time = hours - (numberOfDays * hoursInDay);
-        c.add(Calendar.HOUR, time);
-        handleWeekend(c, true);
-        handleHoliday(c, hours > 0 || min > 0);
-
-        currentCalHour = c.get(Calendar.HOUR_OF_DAY);
-        if (currentCalHour >= endHour) {
-            c.add(Calendar.DAY_OF_YEAR, 1);
-            // set hour to the starting one
-            c.set(Calendar.HOUR_OF_DAY, startHour);
-            c.add(Calendar.HOUR_OF_DAY, currentCalHour - endHour);
-        } else if (currentCalHour < startHour) {
-            c.add(Calendar.HOUR_OF_DAY, startHour - currentCalHour);
-            c.set(Calendar.MINUTE, 0);
-            c.set(Calendar.SECOND, 0);
-        }
+        calendar.add(Calendar.HOUR, time);
+        logger.trace("calendar after adding time {}: {}", time, 
calendar.getTime());
+        boolean resetTime = true;
+        rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar, 
weekendDays, resetTime);
+        logger.trace("calendar after rolling to next working day: {}", 
calendar.getTime());
+        rollCalendarAfterHolidays(calendar, holidays, weekendDays, hours > 0 
|| min > 0);
+        logger.trace("calendar after holidays: {}", calendar.getTime());
+        rollCalendarToWorkingHour(calendar, false);
+        logger.trace("calendar after rolling to working hour: {}", 
calendar.getTime());
 
         // calculate minutes
         int numberOfHours = min / 60;
         if (numberOfHours > 0) {
-            c.add(Calendar.HOUR, numberOfHours);
+            calendar.add(Calendar.HOUR, numberOfHours);
             min = min - (numberOfHours * 60);
         }
-        c.add(Calendar.MINUTE, min);
+        calendar.add(Calendar.MINUTE, min);
 
         // calculate seconds
         int numberOfMinutes = sec / 60;
         if (numberOfMinutes > 0) {
-            c.add(Calendar.MINUTE, numberOfMinutes);
+            calendar.add(Calendar.MINUTE, numberOfMinutes);
             sec = sec - (numberOfMinutes * 60);
         }
-        c.add(Calendar.SECOND, sec);
+        calendar.add(Calendar.SECOND, sec);
+        logger.trace("calendar after adding {} hour, {} minutes and {} 
seconds: {}", numberOfHours, numberOfMinutes, sec, calendar.getTime());
+
+        rollCalendarToWorkingHour(calendar, false);
+        logger.trace("calendar after rolling to next working day: {}", 
calendar.getTime());
+
+        // take under consideration weekend
+        resetTime = false;
+        rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar, 
weekendDays, resetTime);
+        logger.trace("calendar after rolling to next working day: {}", 
calendar.getTime());
+        // take under consideration holidays
+        rollCalendarAfterHolidays(calendar, holidays, weekendDays, resetTime);
+        logger.trace("calendar after holidays: {}", calendar.getTime());
+
+        return calendar.getTime();
+    }
+
+    /**
+     * Indirection used only for testing purposes
+     * 
+     * @return
+     */
+    protected Calendar getCalendar() {
+        String debugMessage = testingCalendar != null ? "Returning clone of 
testingCalendar " : "Return new GregorianCalendar";
+        logger.trace(debugMessage);
+        return testingCalendar != null ? (Calendar) testingCalendar.clone() : 
new GregorianCalendar();
+    }
 
-        currentCalHour = c.get(Calendar.HOUR_OF_DAY);
+    /**
+     * Rolls the <code>HOUR_OF_DAY</code> of the given <code>Calendar</code> 
depending on
+     * given <code>currentCalHour</code>, instance <code>endHour</code>, and 
instance <code>startHour</code>
+     *
+     * It also consider if the startHour < endHour (i.e. working daily hours) 
or startHour > endHour (i.e. nightly daily hours).
+     *
+     * The case where startHour = endHour is excluded by validation of the 
<code>CalendarBean</code>
+     * 
+     * @param toRoll
+     * @param resetMinuteSecond if <code>true</code>, set minutes and seconds 
to 0
+     */
+    protected void rollCalendarToWorkingHour(Calendar toRoll, boolean 
resetMinuteSecond) {
+        logger.trace("toRoll: {}", toRoll.getTime());
+        if (startHour < endHour) {
+            rollCalendarToDailyWorkingHour(toRoll, startHour, endHour);
+        } else {
+            throw new UnsupportedOperationException(String.format("This 
feature is not supported yet: %s should be greater than %s", END_HOUR, 
START_HOUR));
+        }
+        if (resetMinuteSecond) {
+            toRoll.set(Calendar.MINUTE, 0);
+            toRoll.set(Calendar.SECOND, 0);
+        }
+    }
+
+    /**
+     * Rolls the <code>HOUR_OF_DAY</code> of the given <code>Calendar</code> 
to the next "daily" working hour
+     *
+     * @param toRoll
+     * @param startHour
+     * @param endHour
+     */
+    static void rollCalendarToDailyWorkingHour(Calendar toRoll, int startHour, 
int endHour) {
+        logger.trace("toRoll: {}", toRoll.getTime());
+        logger.trace("startHour: {}", startHour);
+        logger.trace("endHour: {}", endHour);
+        int currentCalHour = toRoll.get(Calendar.HOUR_OF_DAY);
         if (currentCalHour >= endHour) {
-            c.add(Calendar.DAY_OF_YEAR, 1);
+            toRoll.add(Calendar.DAY_OF_YEAR, 1);
             // set hour to the starting one
-            c.set(Calendar.HOUR_OF_DAY, startHour);
-            c.add(Calendar.HOUR_OF_DAY, currentCalHour - endHour);
+            toRoll.set(Calendar.HOUR_OF_DAY, startHour);
         } else if (currentCalHour < startHour) {
-            c.add(Calendar.HOUR_OF_DAY, startHour - currentCalHour);
-            c.set(Calendar.MINUTE, 0);
-            c.set(Calendar.SECOND, 0);
+            toRoll.add(Calendar.HOUR_OF_DAY, startHour - currentCalHour);
         }
-        // take under consideration weekend
-        handleWeekend(c, false);
-        // take under consideration holidays
-        handleHoliday(c, false);
+        logger.trace("calendar after rolling to daily working hour: {}", 
toRoll.getTime());
+    }
 
-        return c.getTime();
+    /**
+     * Rolls the <code>HOUR_OF_DAY</code> of the given <code>Calendar</code> 
to the next "nightly" working hour
+     *
+     * @param toRoll
+     * @param startHour
+     * @param endHour
+     */
+    static void rollCalendarToNightlyWorkingHour(Calendar toRoll, int 
startHour, int endHour) {
+        logger.trace("toRoll: {}", toRoll.getTime());
+        logger.trace("startHour: {}", startHour);
+        logger.trace("endHour: {}", endHour);
+        int currentCalHour = toRoll.get(Calendar.HOUR_OF_DAY);
+        if (currentCalHour < endHour) {
+            toRoll.set(Calendar.HOUR_OF_DAY, endHour);
+        } else if (currentCalHour >= startHour) {
+            toRoll.add(Calendar.DAY_OF_YEAR, 1);
+            toRoll.set(Calendar.HOUR_OF_DAY, endHour);
+        }
+        toRoll.set(Calendar.MINUTE, 0);
+        toRoll.set(Calendar.SECOND, 0);
+        logger.debug("calendar after rolling to nightly working hour: {}", 
toRoll.getTime());
     }
 
-    protected void handleHoliday(Calendar c, boolean resetTime) {
+    /**
+     * Rolls the given <code>Calendar</code> to the first <b>working day</b>
+     * after configured <code>holidays</code>, if provided.
+     *
+     * Set hour, minute, second and millisecond when
+     * <code>resetTime</code> is <code>true</code>
+     * 
+     * @param toRoll
+     * @param holidays
+     * @param resetTime
+     */
+    static void rollCalendarAfterHolidays(Calendar toRoll, List<TimePeriod> 
holidays, List<Integer> weekendDays, boolean resetTime) {
+        logger.trace("toRoll: {}", toRoll.getTime());
+        logger.trace("holidays: {}", holidays);
+        logger.trace("weekendDays: {}", weekendDays);
+        logger.trace("resetTime: {}", resetTime);
         if (!holidays.isEmpty()) {
-            Date current = c.getTime();
+            Date current = toRoll.getTime();
             for (TimePeriod holiday : holidays) {
                 // check each holiday if it overlaps current date and break 
after first match
                 if (current.after(holiday.getFrom()) && 
current.before(holiday.getTo())) {
 
-                    Calendar tmp = new GregorianCalendar();
-                    tmp.setTime(holiday.getTo());
+                    Calendar lastHolidayDayTime = new GregorianCalendar();
+                    lastHolidayDayTime.setTime(holiday.getTo());
 
-                    Calendar tmp2 = new GregorianCalendar();
-                    tmp2.setTime(current);
-                    tmp2.set(Calendar.HOUR_OF_DAY, 0);
-                    tmp2.set(Calendar.MINUTE, 0);
-                    tmp2.set(Calendar.SECOND, 0);
-                    tmp2.set(Calendar.MILLISECOND, 0);
+                    Calendar currentDayTmp = new GregorianCalendar();
+                    currentDayTmp.setTime(current);
+                    currentDayTmp.set(Calendar.HOUR_OF_DAY, 0);
+                    currentDayTmp.set(Calendar.MINUTE, 0);
+                    currentDayTmp.set(Calendar.SECOND, 0);
+                    currentDayTmp.set(Calendar.MILLISECOND, 0);
 
-                    long difference = tmp.getTimeInMillis() - 
tmp2.getTimeInMillis();
+                    long difference = lastHolidayDayTime.getTimeInMillis() - 
currentDayTmp.getTimeInMillis();
+                    int dayDifference = (int) Math.ceil(difference / 
(HOUR_IN_MILLIS * 24d));
 
-                    c.add(Calendar.HOUR_OF_DAY, (int) (difference / 
HOUR_IN_MILLIS));
+                    toRoll.add(Calendar.DAY_OF_MONTH, dayDifference);
 
-                    handleWeekend(c, resetTime);
+                    
rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(toRoll, weekendDays, 
resetTime);
                     break;
                 }
             }
@@ -340,122 +371,113 @@ public class BusinessCalendarImpl implements 
BusinessCalendar {
 
     }
 
-    protected int getPropertyAsInt(String propertyName, String defaultValue) {
-        String value = businessCalendarConfiguration.getProperty(propertyName, 
defaultValue);
+    /**
+     * Rolls the given <code>Calendar</code> to the first <b>working day</b>
+     * Set hour, minute, second and millisecond when
+     * <code>resetTime</code> is <code>true</code>
+     * 
+     * @param toRoll
+     * @param resetTime
+     */
+    static void rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(Calendar 
toRoll, List<Integer> weekendDays, boolean resetTime) {
+        logger.trace("toRoll: {}", toRoll.getTime());
+        logger.trace("weekendDays: {}", weekendDays);
+        logger.trace("resetTime: {}", resetTime);
+        int dayOfTheWeek = toRoll.get(Calendar.DAY_OF_WEEK);
+        logger.trace("dayOfTheWeek: {}", dayOfTheWeek);
+        while (!isWorkingDay(weekendDays, dayOfTheWeek)) {
+            toRoll.add(Calendar.DAY_OF_YEAR, 1);
+            if (resetTime) {
+                toRoll.set(Calendar.HOUR_OF_DAY, 0);
+                toRoll.set(Calendar.MINUTE, 0);
+                toRoll.set(Calendar.SECOND, 0);
+                toRoll.set(Calendar.MILLISECOND, 0);
+            }
+            dayOfTheWeek = toRoll.get(Calendar.DAY_OF_WEEK);
+        }
+        logger.trace("dayOfTheWeek after rolling calendar: {}", dayOfTheWeek);
+    }
 
-        return Integer.parseInt(value);
+    static boolean isWorkingDay(List<Integer> weekendDays, int day) {
+        logger.trace("weekendDays: {}", weekendDays);
+        logger.trace("day: {}", day);
+        return !weekendDays.contains(day);
     }
 
-    protected List<TimePeriod> parseHolidays() {
-        String holidaysString = 
businessCalendarConfiguration.getProperty(HOLIDAYS);
-        List<TimePeriod> holidays = new ArrayList<>();
-        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
-        if (holidaysString != null) {
-            String[] hPeriods = holidaysString.split(",");
-            SimpleDateFormat sdf = new 
SimpleDateFormat(businessCalendarConfiguration.getProperty(HOLIDAY_DATE_FORMAT, 
"yyyy-MM-dd"));
-            for (String hPeriod : hPeriods) {
-                boolean addNextYearHolidays = false;
-
-                String[] fromTo = hPeriod.split(":");
-                if (fromTo[0].startsWith("*")) {
-                    addNextYearHolidays = true;
-
-                    fromTo[0] = fromTo[0].replaceFirst("\\*", currentYear + 
"");
-                }
-                try {
-                    if (fromTo.length == 2) {
-                        Calendar tmpFrom = new GregorianCalendar();
-                        if (timezone != null) {
-                            
tmpFrom.setTimeZone(TimeZone.getTimeZone(timezone));
-                        }
-                        tmpFrom.setTime(sdf.parse(fromTo[0]));
-
-                        if (fromTo[1].startsWith("*")) {
-
-                            fromTo[1] = fromTo[1].replaceFirst("\\*", 
currentYear + "");
-                        }
-
-                        Calendar tmpTo = new GregorianCalendar();
-                        if (timezone != null) {
-                            tmpTo.setTimeZone(TimeZone.getTimeZone(timezone));
-                        }
-                        tmpTo.setTime(sdf.parse(fromTo[1]));
-                        Date from = tmpFrom.getTime();
-
-                        tmpTo.add(Calendar.DAY_OF_YEAR, 1);
-
-                        if ((tmpFrom.get(Calendar.MONTH) > 
tmpTo.get(Calendar.MONTH)) && (tmpFrom.get(Calendar.YEAR) == 
tmpTo.get(Calendar.YEAR))) {
-                            tmpTo.add(Calendar.YEAR, 1);
-                        }
-
-                        Date to = tmpTo.getTime();
-                        holidays.add(new TimePeriod(from, to));
-
-                        holidays.add(new TimePeriod(from, to));
-                        if (addNextYearHolidays) {
-                            tmpFrom = new GregorianCalendar();
-                            if (timezone != null) {
-                                
tmpFrom.setTimeZone(TimeZone.getTimeZone(timezone));
-                            }
-                            tmpFrom.setTime(sdf.parse(fromTo[0]));
-                            tmpFrom.add(Calendar.YEAR, 1);
-
-                            from = tmpFrom.getTime();
-                            tmpTo = new GregorianCalendar();
-                            if (timezone != null) {
-                                
tmpTo.setTimeZone(TimeZone.getTimeZone(timezone));
-                            }
-                            tmpTo.setTime(sdf.parse(fromTo[1]));
-                            tmpTo.add(Calendar.YEAR, 1);
-                            tmpTo.add(Calendar.DAY_OF_YEAR, 1);
-
-                            if ((tmpFrom.get(Calendar.MONTH) > 
tmpTo.get(Calendar.MONTH)) && (tmpFrom.get(Calendar.YEAR) == 
tmpTo.get(Calendar.YEAR))) {
-                                tmpTo.add(Calendar.YEAR, 1);
-                            }
-
-                            to = tmpTo.getTime();
-                            holidays.add(new TimePeriod(from, to));
-                        }
-                    } else {
-
-                        Calendar c = new GregorianCalendar();
-                        c.setTime(sdf.parse(fromTo[0]));
-                        c.add(Calendar.DAY_OF_YEAR, 1);
-                        // handle one day holiday
-                        holidays.add(new TimePeriod(sdf.parse(fromTo[0]), 
c.getTime()));
-                        if (addNextYearHolidays) {
-                            Calendar tmp = Calendar.getInstance();
-                            tmp.setTime(sdf.parse(fromTo[0]));
-                            tmp.add(Calendar.YEAR, 1);
-
-                            Date from = tmp.getTime();
-                            c.add(Calendar.YEAR, 1);
-                            holidays.add(new TimePeriod(from, c.getTime()));
-                        }
-                    }
-                } catch (Exception e) {
-                    logger.error("Error while parsing holiday in business 
calendar", e);
-                }
+    protected long getCurrentTime() {
+        String debugMessage = testingCalendar != null ? "Returning 
testingCalendar time " : "Return System time";
+        return testingCalendar != null ? testingCalendar.getTimeInMillis() : 
System.currentTimeMillis();
+    }
+
+    protected String adoptISOFormat(String timeExpression) {
+        logger.trace("timeExpression: {}", timeExpression);
+        try {
+            Duration p = null;
+            if (DateTimeUtils.isPeriod(timeExpression)) {
+                p = Duration.parse(timeExpression);
+            } else if (DateTimeUtils.isNumeric(timeExpression)) {
+                p = Duration.of(Long.valueOf(timeExpression), 
ChronoUnit.MILLIS);
+            } else {
+                OffsetDateTime dateTime = OffsetDateTime.parse(timeExpression, 
DateTimeFormatter.ISO_DATE_TIME);
+                p = Duration.between(OffsetDateTime.now(), dateTime);
+            }
+
+            long days = p.toDays();
+            long hours = p.toHours() % 24;
+            long minutes = p.toMinutes() % 60;
+            long seconds = p.getSeconds() % 60;
+            long milis = p.toMillis() % 1000;
+
+            StringBuffer time = new StringBuffer();
+            if (days > 0) {
+                time.append(days + "d");
+            }
+            if (hours > 0) {
+                time.append(hours + "h");
+            }
+            if (minutes > 0) {
+                time.append(minutes + "m");
             }
+            if (seconds > 0) {
+                time.append(seconds + "s");
+            }
+            if (milis > 0) {
+                time.append(milis + "ms");
+            }
+
+            return time.toString();
+        } catch (Exception e) {
+            return timeExpression;
         }
-        return holidays;
     }
 
-    protected void parseWeekendDays() {
-        String weekendDays = 
businessCalendarConfiguration.getProperty(WEEKEND_DAYS);
+    public static class Builder {
 
-        if (weekendDays == null) {
-            this.weekendDays.add(Calendar.SATURDAY);
-            this.weekendDays.add(Calendar.SUNDAY);
-        } else {
-            String[] days = weekendDays.split(",");
-            for (String day : days) {
-                this.weekendDays.add(Integer.parseInt(day));
-            }
+        private CalendarBean calendarBean;
+        private Calendar testingCalendar;
+
+        public Builder withCalendarBean(CalendarBean calendarBean) {
+            this.calendarBean = calendarBean;
+            return this;
+        }
+
+        /**
+         * Used only for testing purposes.
+         * 
+         * @param testingCalendar
+         * @return
+         */
+        public Builder withTestingCalendar(Calendar testingCalendar) {
+            this.testingCalendar = testingCalendar;
+            return this;
+        }
+
+        public BusinessCalendarImpl build() {
+            return calendarBean == null ? new 
BusinessCalendarImpl(testingCalendar) : new BusinessCalendarImpl(calendarBean, 
testingCalendar);
         }
     }
 
-    private class TimePeriod {
+    static class TimePeriod {
         private Date from;
         private Date to;
 
@@ -471,35 +493,27 @@ public class BusinessCalendarImpl implements 
BusinessCalendar {
         protected Date getTo() {
             return this.to;
         }
-    }
 
-    protected long getCurrentTime() {
-        if (clock != null) {
-            return clock.getCurrentTime();
-        } else {
-            return System.currentTimeMillis();
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof TimePeriod that)) {
+                return false;
+            }
+            return Objects.equals(from, that.from) && Objects.equals(to, 
that.to);
         }
-    }
 
-    protected boolean isWorkingDay(int day) {
-        if (weekendDays.contains(day)) {
-            return false;
+        @Override
+        public int hashCode() {
+            return Objects.hash(from, to);
         }
 
-        return true;
-    }
-
-    protected void handleWeekend(Calendar c, boolean resetTime) {
-        int dayOfTheWeek = c.get(Calendar.DAY_OF_WEEK);
-        while (!isWorkingDay(dayOfTheWeek)) {
-            c.add(Calendar.DAY_OF_YEAR, 1);
-            if (resetTime) {
-                c.set(Calendar.HOUR_OF_DAY, 0);
-                c.set(Calendar.MINUTE, 0);
-                c.set(Calendar.SECOND, 0);
-                c.set(Calendar.MILLISECOND, 0);
-            }
-            dayOfTheWeek = c.get(Calendar.DAY_OF_WEEK);
+        @Override
+        public String toString() {
+            return "TimePeriod{" +
+                    "from=" + from +
+                    ", to=" + to +
+                    '}';
         }
     }
+
 }
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBean.java 
b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBean.java
new file mode 100644
index 0000000000..7ca90c904d
--- /dev/null
+++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBean.java
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.jbpm.process.core.timer;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.END_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAYS;
+import static 
org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAY_DATE_FORMAT;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.START_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.TIMEZONE;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.WEEKEND_DAYS;
+
+public class CalendarBean {
+
+    // Default access for testing purpose
+    static final List<Integer> DEFAULT_WEEKEND_DAYS = 
Arrays.asList(Calendar.SATURDAY, Calendar.SUNDAY);
+    static final String DEFAULT_WEEKENDS = 
DEFAULT_WEEKEND_DAYS.stream().map(String::valueOf).collect(Collectors.joining(","));
+    static final String DEFAULT_HOLIDAY_DATE_FORMAT = "yyyy-MM-dd";
+    static final String DEFAULT_TIMEZONE = TimeZone.getDefault().getID();
+
+    private static final Logger logger = 
LoggerFactory.getLogger(CalendarBean.class);
+    private static final Collection<String> REQUIRED_PROPERTIES = 
Arrays.asList(START_HOUR, END_HOUR);
+
+    private static final Map<String, BiConsumer<StringBuilder, Properties>> 
FORMAT_VALIDATOR_MAP;
+    private static final List<BiConsumer<StringBuilder, Properties>> 
BUSINESS_VALIDATOR_LIST;
+
+    private static final int LOWER_HOUR_BOUND = 0;
+
+    private static final int UPPER_HOUR_BOUND = 24;
+
+    private static final String OUTSIDE_BOUNDARY_ERROR_MESSAGE = "%s %s 
outside expected boundaries %s";
+    private static final String INVALID_FORMAT_ERROR_MESSAGE = "%s is not 
valid: %s";
+    private static final String REPEATED_VALUES_ERROR_MESSAGE = "There are 
repeated values in the given %s %s";
+    private static final String OTHER_VALUES_ERR_MSG = "%s and other values 
provided in the given %s %s";
+    private static final String VALUES_SAME_ERR_MSG = "%s %s and %s %s must be 
different";
+    private static final String PROPERTY_REQUIRED_ERR_MSG = "Property %s is 
required";
+
+    private final Properties calendarConfiguration;
+
+    static {
+        FORMAT_VALIDATOR_MAP = new HashMap<>();
+        FORMAT_VALIDATOR_MAP.put(START_HOUR, (stringBuilder, properties) -> {
+            if (properties.containsKey(START_HOUR)) {
+                try {
+                    int hour = getPropertyAsInt(START_HOUR, properties);
+                    if (!isInsideValidRange(hour, LOWER_HOUR_BOUND, 
UPPER_HOUR_BOUND)) {
+                        addMessageToStringBuilder(stringBuilder, 
String.format(OUTSIDE_BOUNDARY_ERROR_MESSAGE, START_HOUR, hour, "(0-24)"));
+                    }
+                } catch (NumberFormatException e) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(INVALID_FORMAT_ERROR_MESSAGE, START_HOUR, e.getMessage()));
+                }
+            }
+        });
+        FORMAT_VALIDATOR_MAP.put(END_HOUR, (stringBuilder, properties) -> {
+            if (properties.containsKey(END_HOUR)) {
+                try {
+                    int hour = getPropertyAsInt(END_HOUR, properties);
+                    if (!isInsideValidRange(hour, LOWER_HOUR_BOUND, 
UPPER_HOUR_BOUND)) {
+                        addMessageToStringBuilder(stringBuilder, 
String.format(OUTSIDE_BOUNDARY_ERROR_MESSAGE, END_HOUR, hour, "(0-24)"));
+                    }
+                } catch (NumberFormatException e) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(INVALID_FORMAT_ERROR_MESSAGE, END_HOUR, e.getMessage()));
+                }
+            }
+        });
+        FORMAT_VALIDATOR_MAP.put(HOLIDAYS, (stringBuilder, properties) -> {
+            if (properties.containsKey(HOLIDAYS)) {
+                String originalData = properties.getProperty(HOLIDAYS);
+                String[] allHolidays = originalData.split(",");
+                for (String holiday : allHolidays) {
+                    String[] ranges = holiday.split(":");
+                    for (String range : ranges) {
+                        try {
+                            getFormattedDate(range, properties);
+                        } catch (ParseException e) {
+                            addMessageToStringBuilder(stringBuilder, 
String.format(INVALID_FORMAT_ERROR_MESSAGE, HOLIDAYS, e.getMessage()));
+                        }
+                    }
+                }
+            }
+        });
+        FORMAT_VALIDATOR_MAP.put(HOLIDAY_DATE_FORMAT, (stringBuilder, 
properties) -> {
+            if (properties.containsKey(HOLIDAY_DATE_FORMAT)) {
+                try {
+                    getSimpleDateFormat((String) 
properties.get(HOLIDAY_DATE_FORMAT));
+                } catch (IllegalArgumentException e) {
+                    addMessageToStringBuilder(stringBuilder, e.getMessage());
+                }
+            }
+        });
+        FORMAT_VALIDATOR_MAP.put(WEEKEND_DAYS, (stringBuilder, properties) -> {
+            if (properties.containsKey(WEEKEND_DAYS)) {
+                String originalData = properties.getProperty(WEEKEND_DAYS);
+                String[] weekendDays = originalData.split(",\\s?");
+                Set<String> differentValues = 
Arrays.stream(weekendDays).collect(Collectors.toSet());
+                if (differentValues.size() < weekendDays.length) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(REPEATED_VALUES_ERROR_MESSAGE, WEEKEND_DAYS, originalData));
+                }
+                if (differentValues.contains("0") && differentValues.size() > 
1) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(OTHER_VALUES_ERR_MSG, "0 (= no weekends)", WEEKEND_DAYS, 
originalData));
+                }
+                final List<Integer> intValues = new ArrayList<>();
+                differentValues.forEach(s -> {
+                    try {
+                        intValues.add(getStringAsInt(s));
+                    } catch (NumberFormatException e) {
+                        addMessageToStringBuilder(stringBuilder, 
e.getMessage());
+                    }
+                });
+                if (intValues.stream().anyMatch(value -> value < 0 || value > 
7)) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(OUTSIDE_BOUNDARY_ERROR_MESSAGE, WEEKEND_DAYS, 
intValues.stream().filter(value -> value < 0 || value > 7).toList(), "(0-7)"));
+                }
+            }
+        });
+        FORMAT_VALIDATOR_MAP.put(TIMEZONE, (stringBuilder, properties) -> {
+            if (properties.containsKey(TIMEZONE)) {
+                String originalData = properties.getProperty(TIMEZONE);
+                if 
(!Arrays.asList(TimeZone.getAvailableIDs()).contains(originalData)) {
+                    addMessageToStringBuilder(stringBuilder, 
String.format(INVALID_FORMAT_ERROR_MESSAGE, TIMEZONE, originalData));
+                }
+            }
+        });
+        BUSINESS_VALIDATOR_LIST = new ArrayList<>();
+        BUSINESS_VALIDATOR_LIST.add((stringBuilder, properties) -> {
+            if (properties.containsKey(START_HOUR) && 
properties.containsKey(END_HOUR)) {
+                try {
+                    int startHour = getPropertyAsInt(START_HOUR, properties);
+                    int endHour = getPropertyAsInt(END_HOUR, properties);
+                    if (startHour == endHour) {
+                        addMessageToStringBuilder(stringBuilder, 
String.format(VALUES_SAME_ERR_MSG, START_HOUR, startHour, END_HOUR, endHour));
+                    }
+                } catch (NumberFormatException nfe) {
+                    logger.error("Number format exception while checking 
equality of start time and end time: {}", nfe.getMessage());
+                }
+            }
+        });
+    }
+
+    public CalendarBean(Properties calendarConfiguration) {
+        this.calendarConfiguration = calendarConfiguration;
+        setup();
+    }
+
+    static void formalValidation(StringBuilder errorMessage, Properties 
calendarConfiguration) {
+        requiredPropertyValidation(errorMessage, calendarConfiguration);
+        propertyFormatValidation(errorMessage, calendarConfiguration);
+    }
+
+    static void requiredPropertyValidation(StringBuilder errorMessage, 
Properties calendarConfiguration) {
+        REQUIRED_PROPERTIES.forEach(property -> 
validateRequiredProperty(property, errorMessage, calendarConfiguration));
+    }
+
+    static void propertyFormatValidation(StringBuilder errorMessage, 
Properties calendarConfiguration) {
+        
FORMAT_VALIDATOR_MAP.values().forEach(stringBuilderPropertiesBiConsumer -> 
stringBuilderPropertiesBiConsumer.accept(errorMessage, calendarConfiguration));
+    }
+
+    static void businessValidation(StringBuilder errorMessage, Properties 
calendarConfiguration) {
+        BUSINESS_VALIDATOR_LIST.forEach(stringBuilderPropertiesBiConsumer -> 
stringBuilderPropertiesBiConsumer.accept(errorMessage, calendarConfiguration));
+    }
+
+    static void missingDataPopulation(Properties calendarConfiguration) {
+        if (!calendarConfiguration.containsKey(WEEKEND_DAYS)) {
+            calendarConfiguration.put(WEEKEND_DAYS, DEFAULT_WEEKENDS);
+        }
+        if (!calendarConfiguration.containsKey(HOLIDAY_DATE_FORMAT)) {
+            calendarConfiguration.put(HOLIDAY_DATE_FORMAT, 
DEFAULT_HOLIDAY_DATE_FORMAT);
+        }
+        if (!calendarConfiguration.containsKey(TIMEZONE)) {
+            calendarConfiguration.put(TIMEZONE, DEFAULT_TIMEZONE);
+        }
+    }
+
+    static int getPropertyAsInt(String propertyName, Properties 
calendarConfiguration) {
+        String value = calendarConfiguration.getProperty(propertyName);
+        return getStringAsInt(value);
+    }
+
+    static int getStringAsInt(String value) {
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException nfe) {
+            logger.error("Number format exception while parsing {} {}", value, 
nfe.getMessage());
+            throw nfe;
+        }
+    }
+
+    static Date getFormattedDate(String date, Properties businessCalendar) 
throws ParseException {
+        SimpleDateFormat sdf =
+                businessCalendar.containsKey(HOLIDAY_DATE_FORMAT) ? 
getSimpleDateFormat(businessCalendar.getProperty(HOLIDAY_DATE_FORMAT)) : 
getSimpleDateFormat(DEFAULT_HOLIDAY_DATE_FORMAT);
+        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+        if (date.startsWith("*")) {
+            date = date.replaceFirst("\\*", currentYear + "");
+        }
+        return sdf.parse(date);
+    }
+
+    static SimpleDateFormat getSimpleDateFormat(String format) throws 
IllegalArgumentException {
+        return new SimpleDateFormat(format);
+    }
+
+    static void validateRequiredProperty(String property, StringBuilder 
errorMessage, Properties calendarConfiguration) {
+        String value = calendarConfiguration.getProperty(property);
+        if (Objects.isNull(value)) {
+            addMessageToStringBuilder(errorMessage, 
String.format(PROPERTY_REQUIRED_ERR_MSG, property));
+        }
+    }
+
+    static boolean isInsideValidRange(int value, int lowerBound, int 
upperBound) {
+        return value >= lowerBound && value <= upperBound;
+    }
+
+    private static void addMessageToStringBuilder(StringBuilder stringBuilder, 
String message) {
+        stringBuilder.append(message);
+        stringBuilder.append("\n");
+    }
+
+    public List<BusinessCalendarImpl.TimePeriod> getHolidays() {
+        if (!calendarConfiguration.containsKey(HOLIDAYS)) {
+            return Collections.emptyList();
+        }
+        String timezone = calendarConfiguration.getProperty(TIMEZONE);
+
+        String holidaysString = calendarConfiguration.getProperty(HOLIDAYS);
+        List<BusinessCalendarImpl.TimePeriod> holidays = new ArrayList<>();
+        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+        String[] hPeriods = holidaysString.split(",");
+
+        for (String hPeriod : hPeriods) {
+            boolean addNextYearHolidays = false;
+
+            String[] fromTo = hPeriod.split(":");
+            if (fromTo[0].startsWith("*")) {
+                addNextYearHolidays = true;
+
+                fromTo[0] = fromTo[0].replaceFirst("\\*", currentYear + "");
+            }
+            try {
+                if (fromTo.length == 2) {
+                    Calendar tmpFrom = new GregorianCalendar();
+                    if (timezone != null) {
+                        tmpFrom.setTimeZone(TimeZone.getTimeZone(timezone));
+                    }
+                    tmpFrom.setTime(getFormattedDate(fromTo[0], 
calendarConfiguration));
+
+                    if (fromTo[1].startsWith("*")) {
+
+                        fromTo[1] = fromTo[1].replaceFirst("\\*", currentYear 
+ "");
+                    }
+
+                    Calendar tmpTo = new GregorianCalendar();
+                    if (timezone != null) {
+                        tmpTo.setTimeZone(TimeZone.getTimeZone(timezone));
+                    }
+                    tmpTo.setTime(getFormattedDate(fromTo[1], 
calendarConfiguration));
+                    Date from = tmpFrom.getTime();
+
+                    tmpTo.add(Calendar.DAY_OF_YEAR, 1);
+
+                    if ((tmpFrom.get(Calendar.MONTH) > 
tmpTo.get(Calendar.MONTH)) && (tmpFrom.get(Calendar.YEAR) == 
tmpTo.get(Calendar.YEAR))) {
+                        tmpTo.add(Calendar.YEAR, 1);
+                    }
+
+                    Date to = tmpTo.getTime();
+                    holidays.add(new BusinessCalendarImpl.TimePeriod(from, 
to));
+
+                    if (addNextYearHolidays) {
+                        tmpFrom = new GregorianCalendar();
+                        if (timezone != null) {
+                            
tmpFrom.setTimeZone(TimeZone.getTimeZone(timezone));
+                        }
+                        tmpFrom.setTime(getFormattedDate(fromTo[0], 
calendarConfiguration));
+                        tmpFrom.add(Calendar.YEAR, 1);
+
+                        from = tmpFrom.getTime();
+                        tmpTo = new GregorianCalendar();
+                        if (timezone != null) {
+                            tmpTo.setTimeZone(TimeZone.getTimeZone(timezone));
+                        }
+                        tmpTo.setTime(getFormattedDate(fromTo[1], 
calendarConfiguration));
+                        tmpTo.add(Calendar.YEAR, 1);
+                        tmpTo.add(Calendar.DAY_OF_YEAR, 1);
+
+                        if ((tmpFrom.get(Calendar.MONTH) > 
tmpTo.get(Calendar.MONTH)) && (tmpFrom.get(Calendar.YEAR) == 
tmpTo.get(Calendar.YEAR))) {
+                            tmpTo.add(Calendar.YEAR, 1);
+                        }
+
+                        to = tmpTo.getTime();
+                        holidays.add(new BusinessCalendarImpl.TimePeriod(from, 
to));
+                    }
+                } else {
+
+                    Calendar c = new GregorianCalendar();
+                    c.setTime(getFormattedDate(fromTo[0], 
calendarConfiguration));
+                    c.add(Calendar.DAY_OF_YEAR, 1);
+                    // handle one day holiday
+                    holidays.add(new 
BusinessCalendarImpl.TimePeriod(getFormattedDate(fromTo[0], 
calendarConfiguration), c.getTime()));
+                    if (addNextYearHolidays) {
+                        Calendar tmp = Calendar.getInstance();
+                        tmp.setTime(getFormattedDate(fromTo[0], 
calendarConfiguration));
+                        tmp.add(Calendar.YEAR, 1);
+
+                        Date from = tmp.getTime();
+                        c.add(Calendar.YEAR, 1);
+                        holidays.add(new BusinessCalendarImpl.TimePeriod(from, 
c.getTime()));
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("Error while parsing holiday in business 
calendar", e);
+            }
+        }
+        return holidays;
+    }
+
+    public List<Integer> getWeekendDays() {
+        return parseWeekendDays(calendarConfiguration);
+    }
+
+    public int getDaysPerWeek() {
+        return 7 - parseWeekendDays(calendarConfiguration).size();
+    }
+
+    public String getTimezone() {
+        return calendarConfiguration.getProperty(TIMEZONE);
+    }
+
+    public int getStartHour() {
+        return getPropertyAsInt(START_HOUR);
+    }
+
+    public int getEndHour() {
+        return getPropertyAsInt(END_HOUR);
+    }
+
+    public int getHoursInDay() {
+        int startHour = getStartHour();
+        int endHour = getEndHour();
+        return startHour < endHour ? endHour - startHour : (24 - startHour) + 
endHour;
+    }
+
+    protected void setup() {
+        StringBuilder errorMessage = new StringBuilder();
+        formalValidation(errorMessage, calendarConfiguration);
+        missingDataPopulation(calendarConfiguration);
+        businessValidation(errorMessage, calendarConfiguration);
+        if (!errorMessage.isEmpty()) {
+            throw new IllegalArgumentException(errorMessage.toString());
+        }
+    }
+
+    protected List<Integer> parseWeekendDays(Properties calendarConfiguration) 
{
+        String weekendDays = calendarConfiguration.getProperty(WEEKEND_DAYS);
+        String[] days = weekendDays.split(",");
+        return Arrays.stream(days).map(day -> Integer.parseInt(day.trim()))
+                .filter(intDay -> intDay != 0)
+                .collect(Collectors.toList());
+    }
+
+    protected int getPropertyAsInt(String propertyName) {
+        return getPropertyAsInt(propertyName, calendarConfiguration);
+    }
+}
\ No newline at end of file
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBeanFactory.java
 
b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBeanFactory.java
new file mode 100644
index 0000000000..bf0eed797a
--- /dev/null
+++ 
b/jbpm/jbpm-flow/src/main/java/org/jbpm/process/core/timer/CalendarBeanFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jbpm.process.core.timer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Objects;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static 
org.jbpm.process.core.constants.CalendarConstants.BUSINESS_CALENDAR_PATH;
+
+public class CalendarBeanFactory {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(CalendarBeanFactory.class);
+
+    public static CalendarBean createCalendarBean() {
+        URL resource = 
Thread.currentThread().getContextClassLoader().getResource(BUSINESS_CALENDAR_PATH);
+        if (Objects.nonNull(resource)) {
+            logger.debug("URL resource: {}", resource);
+            Properties calendarConfiguration = new Properties();
+            try (InputStream is = resource.openStream()) {
+                calendarConfiguration.load(is);
+                return new CalendarBean(calendarConfiguration);
+            } catch (IOException e) {
+                String errorMessage = "Error while loading properties for 
business calendar";
+                logger.error(errorMessage, e);
+                throw new RuntimeException(errorMessage, e);
+            } catch (IllegalArgumentException e) {
+                String errorMessage = "Error while populating properties for 
business calendar";
+                logger.error(errorMessage, e);
+                throw e;
+            }
+        } else {
+            String errorMessage = String.format("Missing %s", 
BUSINESS_CALENDAR_PATH);
+            logger.error(errorMessage);
+            throw new RuntimeException(errorMessage);
+        }
+    }
+}
diff --git 
a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java
 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java
index 40ad261bdd..e1038c8b39 100755
--- 
a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java
+++ 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/BusinessCalendarImplTest.java
@@ -18,491 +18,367 @@
  */
 package org.jbpm.process.core.timer;
 
-import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 import java.util.Properties;
-import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.stream.IntStream;
 
 import org.jbpm.test.util.AbstractBaseTest;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
-import org.kie.kogito.timer.SessionPseudoClock;
 import org.slf4j.LoggerFactory;
 
+import static java.time.temporal.ChronoUnit.DAYS;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.END_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAYS;
+import static 
org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAY_DATE_FORMAT;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.START_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.WEEKEND_DAYS;
 
-public class BusinessCalendarImplTest extends AbstractBaseTest {
+class BusinessCalendarImplTest extends AbstractBaseTest {
 
     public void addLogger() {
         logger = LoggerFactory.getLogger(this.getClass());
     }
 
     @Test
-    public void testCalculateHours() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-04 16:45";
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-04 13:45").getTime());
-
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("3h");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateHoursCustomWorkingHours() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "6");
-        String expectedDate = "2012-05-04 15:45";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-03 13:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("8h");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateHoursPassingOverWeekend() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-07 12:45";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-04 13:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("7h");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateHoursPassingOverCustomDefinedWeekend() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, Calendar.FRIDAY 
+ "," + Calendar.SATURDAY);
-        String expectedDate = "2012-05-06 12:45";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-03 13:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("7h");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateMinutesPassingOverWeekend() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-07 09:15";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-04 16:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("30m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateMinutesPassingOverHoliday() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, 
"2012-05-12:2012-05-19");
-        String expectedDate = "2012-05-21 09:15";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-11 16:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("30m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateDays() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-14 09:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDate("2012-05-04").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("6d");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void instantiate() {
+        BusinessCalendarImpl retrieved = 
BusinessCalendarImpl.builder().build();
+        assertThat(retrieved).isNotNull();
+        retrieved = BusinessCalendarImpl.builder()
+                .withCalendarBean(CalendarBeanFactory.createCalendarBean())
+                .build();
+        assertThat(retrieved).isNotNull();
+
+        Properties calendarConfiguration = new Properties();
+        int startHour = 10;
+        int endHour = 16;
+        calendarConfiguration.put(START_HOUR, String.valueOf(startHour));
+        calendarConfiguration.put(END_HOUR, String.valueOf(endHour));
+        retrieved = BusinessCalendarImpl.builder()
+                .withCalendarBean(new CalendarBean(calendarConfiguration))
+                .build();
+        assertThat(retrieved).isNotNull();
     }
 
     @Test
-    public void testCalculateDaysStartingInWeekend() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-09 09:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDate("2012-05-05").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("2d");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateInsideDailyWorkingHourWithDelay() {
+        int daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 0, 3, 
daysToSkip, null, null);
     }
 
     @Test
-    public void testCalculateDaysCustomWorkingDays() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.DAYS_PER_WEEK, "4");
-        config.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, Calendar.FRIDAY 
+ "," + Calendar.SATURDAY + "," + Calendar.SUNDAY);
-        String expectedDate = "2012-05-15 14:30";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-03 14:30").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("6d");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateInsideDailyWorkingHourWithoutDelay() {
+        int daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 0, 0, 
daysToSkip, null, null);
     }
 
+    @Disabled("TO FIX 
https://github.com/apache/incubator-kie-issues/issues/1651";)
     @Test
-    public void testCalculateDaysMiddleDay() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-11 12:27";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-03 12:27").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("6d");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateInsideNightlyWorkingHour() {
+        int daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(4, -4, 0, 3, 
daysToSkip, null, null);
     }
 
     @Test
-    public void testCalculateDaysHoursMinutes() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-14 14:20";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDate("2012-05-04").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("6d4h80m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateBeforeWorkingHourWithDelay() {
+        int daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(2, 4, -1, 1, 
daysToSkip, null, null);
     }
 
     @Test
-    public void testCalculateTimeDaysHoursMinutesHolidays() {
+    void calculateBusinessTimeAsDateBeforeWorkingHourWithDelayFineGrained() {
+        // lets pretend 2024-11-28 10:48:33 is the current time
+        Calendar testingCalendar = Calendar.getInstance();
+        testingCalendar.set(Calendar.YEAR, 2024);
+        testingCalendar.set(Calendar.MONTH, Calendar.NOVEMBER);
+        testingCalendar.set(Calendar.DAY_OF_MONTH, 28);
+        testingCalendar.set(Calendar.HOUR_OF_DAY, 10);
+        testingCalendar.set(Calendar.MINUTE, 48);
+        testingCalendar.set(Calendar.SECOND, 33);
+
+        int startHour = 14;
         Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, 
"2012-05-10:2012-05-19");
-        String expectedDate = "2012-05-21 14:20";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDate("2012-05-04").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("6d4h80m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateTimeDaysHoursMinutesSingleDayHolidays() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, "2012-05-07");
-        String expectedDate = "2012-05-08 13:20";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDate("2012-05-04").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("1d4h20m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void 
testCalculateTimeDaysHoursMinutesSingleDayHolidaysInMiddleOfWeek() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, "2012-05-09");
-        String expectedDate = "2012-05-10 15:30";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-08 11:10").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("1d4h20m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateDaysPassingOverHolidayAtYearEnd() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, 
"2012-12-31:2013-01-01");
-        String expectedDate = "2013-01-04 09:15";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-12-28 16:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("2d30m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
-    }
+        config.setProperty(BusinessCalendarImpl.START_HOUR, 
String.valueOf(startHour));
+        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
+        config.setProperty(WEEKEND_DAYS, "0");
 
-    @Test
-    public void testCalculateDaysPassingOverHolidayAtYearEndWithWildcards() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, "*-12-31:*-01-01");
-        String expectedDate = "2013-01-02 09:15";
+        String delay = "10m";
+        BusinessCalendarImpl businessCal = 
BusinessCalendarImpl.builder().withCalendarBean(new CalendarBean(config))
+                .withTestingCalendar(testingCalendar)
+                .build();
+        Date retrieved = businessCal.calculateBusinessTimeAsDate(delay);
+        String expectedDate = "2024-11-28 14:10:00";
 
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-12-28 16:45").getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String retrievedTime = sdf.format(retrieved);
+        assertThat(retrievedTime).isEqualTo(expectedDate);
 
-        Date result = businessCal.calculateBusinessTimeAsDate("2d30m");
+        delay = "10s";
+        retrieved = businessCal.calculateBusinessTimeAsDate(delay);
+        expectedDate = "2024-11-28 14:00:10";
+        retrievedTime = sdf.format(retrieved);
+        assertThat(retrievedTime).isEqualTo(expectedDate);
 
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+        delay = "10m 10s";
+        retrieved = businessCal.calculateBusinessTimeAsDate(delay);
+        expectedDate = "2024-11-28 14:10:10";
+        retrievedTime = sdf.format(retrieved);
+        assertThat(retrievedTime).isEqualTo(expectedDate);
     }
 
     @Test
-    public void testCalculateISOHours() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-04 16:45";
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-04 13:45").getTime());
-
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("PT3H");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateBeforeWorkingHourWithoutDelay() {
+        int daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-1, 4, -2, 1, 
daysToSkip, null, null);
     }
 
     @Test
-    public void testCalculateISODaysAndHours() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, "2012-05-09");
-        String expectedDate = "2012-05-10 15:30";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime("2012-05-08 11:10").getTime());
-
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("P1DT4H20M");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateAfterWorkingHour() {
+        int daysToSkip = 1; // because the executionHourDelay is bigger to 
endHOurGap, so it goes to next day;
+        commonCalculateBusinessTimeAsDateAssertAtStartHour(-1, 2, 3, 3, 
daysToSkip, null, null);
+        commonCalculateBusinessTimeAsDateAssertAtStartHour(0, 6, 1, 5, 
daysToSkip, null, null);
     }
 
     @Test
-    public void testSingleHolidayWithinGivenTime() {
-        final Properties props = new Properties();
-        props.put(BusinessCalendarImpl.HOLIDAYS, "2015-01-13");
-        String expectedDate = "2015-01-15 11:38";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis("2015-01-08 
11:38:30.198").getTime());
-
-        BusinessCalendarImpl businessCalendarImpl = new 
BusinessCalendarImpl(props, clock);
-
-        Date result = businessCalendarImpl.calculateBusinessTimeAsDate("4d");
-        assertThat(formatDate("yyyy-MM-dd HH:mm", 
result)).isEqualTo(expectedDate);
+    void calculateBusinessTimeAsDateWhenTodayAndTomorrowAreHolidays() {
+        String holidayDateFormat = "yyyy-MM-dd";
+        DateTimeFormatter sdf = DateTimeFormatter.ofPattern(holidayDateFormat);
+        LocalDate today = LocalDate.now();
+        LocalDate tomorrow = today.plusDays(1);
+        String holidays = sdf.format(today) + "," + sdf.format(tomorrow);
+        int daysToSkip = 2; // because both today and tomorrow are holiday
+        // endHOurGap and executionHourDelay are not relevant in this context
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 0, 3, 
daysToSkip, holidayDateFormat, holidays);
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 5, 3, 
daysToSkip, holidayDateFormat, holidays);
     }
 
     @Test
-    public void testCalculateMillisecondsAsDefault() {
-        Properties config = new Properties();
-        String expectedDate = "2012-05-04 16:45:10.000";
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis("2012-05-04 
16:45:00.000").getTime());
-
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("10000");
+    void calculateBusinessTimeAsDateWhenNextDayIsHoliday() {
+        String holidayDateFormat = "yyyy-MM-dd";
+        DateTimeFormatter sdf = DateTimeFormatter.ofPattern(holidayDateFormat);
+        LocalDate tomorrow = LocalDate.now().plusDays(1);
+        String holidays = sdf.format(tomorrow);
+        // 1 because the executionHourDelay is equal to endHOurGap, so it goes 
to next day;
+        // 1 because next day is holiday
+        int daysToSkip = 2;
 
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss.SSS", 
result)).isEqualTo(expectedDate);
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 0, 4, 
daysToSkip, holidayDateFormat, holidays);
+        daysToSkip = 0; // since executionHourDelay falls before endHOurGap
+        commonCalculateBusinessTimeAsDateAssertBetweenHours(-4, 4, 0, 3, 
daysToSkip, holidayDateFormat, holidays);
     }
 
     @Test
-    public void testCalculateMinutesPassingAfterHour() {
-        Properties config = new Properties();
-        String currentDate = "2018-05-02 19:51:33";
-        String expectedDate = "2018-05-03 09:01:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate("1m");
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
-    }
-
+    void rollCalendarToDailyWorkingHour() {
+        int startHour = 14;
+        int endHour = 16;
+        Calendar toRoll = Calendar.getInstance();
+        int currentHour = 8;
+        toRoll.set(Calendar.HOUR_OF_DAY, currentHour);
+        int dayOfYear = toRoll.get(Calendar.DAY_OF_YEAR);
+        BusinessCalendarImpl.rollCalendarToDailyWorkingHour(toRoll, startHour, 
endHour);
+        assertThat(toRoll.get(Calendar.HOUR_OF_DAY)).isEqualTo(startHour);
+        assertThat(toRoll.get(Calendar.DAY_OF_YEAR)).isEqualTo(dayOfYear);
+
+        toRoll = Calendar.getInstance();
+        currentHour = 19;
+        toRoll.set(Calendar.HOUR_OF_DAY, currentHour);
+        dayOfYear = toRoll.get(Calendar.DAY_OF_YEAR);
+        BusinessCalendarImpl.rollCalendarToDailyWorkingHour(toRoll, startHour, 
endHour);
+        assertThat(toRoll.get(Calendar.HOUR_OF_DAY)).isEqualTo(startHour);
+        assertThat(toRoll.get(Calendar.DAY_OF_YEAR)).isEqualTo(dayOfYear + 1);
+    }
+
+    @Disabled("TO FIX 
https://github.com/apache/incubator-kie-issues/issues/1651";)
     @Test
-    public void testBusinessCalendarWithoutProvidedConfiguration() {
-        assertDoesNotThrow(() -> new BusinessCalendarImpl());
-    }
-
-    @Test
-    public void testCalculateMinutesPassingHoliday() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.DAYS_PER_WEEK, "5");
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "8");
-        config.setProperty(BusinessCalendarImpl.START_HOUR, "9");
-        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
-        config.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, "1,7"); // 
sun,sat
-        config.setProperty(BusinessCalendarImpl.HOLIDAYS, 
"2018-04-30,2018-05-03:2018-05-05");
-        config.setProperty(BusinessCalendarImpl.HOLIDAY_DATE_FORMAT, 
"yyyy-MM-dd");
-        String currentDate = "2018-05-03 13:51:33";
-        String duration = "10m";
-        String expectedDate = "2018-05-07 09:10:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
+    void rollCalendarToNightlyWorkingHour() {
+        int startHour = 20;
+        int endHour = 4;
+        Calendar toRoll = Calendar.getInstance();
+        int currentHour = 21;
+        toRoll.set(Calendar.HOUR_OF_DAY, currentHour);
+        int dayOfYear = toRoll.get(Calendar.DAY_OF_YEAR);
+        BusinessCalendarImpl.rollCalendarToNightlyWorkingHour(toRoll, 
startHour, endHour);
+        assertThat(toRoll.get(Calendar.HOUR_OF_DAY)).isEqualTo(startHour);
+        assertThat(toRoll.get(Calendar.DAY_OF_YEAR)).isEqualTo(dayOfYear);
+
+        toRoll = Calendar.getInstance();
+        currentHour = 3;
+        toRoll.set(Calendar.HOUR_OF_DAY, currentHour);
+        dayOfYear = toRoll.get(Calendar.DAY_OF_YEAR);
+        BusinessCalendarImpl.rollCalendarToNightlyWorkingHour(toRoll, 
startHour, endHour);
+        assertThat(toRoll.get(Calendar.HOUR_OF_DAY)).isEqualTo(startHour);
+        assertThat(toRoll.get(Calendar.DAY_OF_YEAR)).isEqualTo(dayOfYear + 1);
 
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
     }
 
     @Test
-    public void testCalculateMinutesPassingWeekend() {
-        Properties config = new Properties();
-        String currentDate = "2018-05-06 13:51:33";
-        String duration = "10m";
-        String expectedDate = "2018-05-07 09:10:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTime(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
+    void rollCalendarAfterHolidays() {
+        Instant now = Instant.now();
+        int holidayLeft = 4;
+        Instant startHolidayInstant = now.minus(2, DAYS);
+        Instant endHolidayInstant = now.plus(holidayLeft, DAYS);
+        Date startHoliday = Date.from(startHolidayInstant);
+        Date endHoliday = Date.from(endHolidayInstant);
+        List<BusinessCalendarImpl.TimePeriod> holidays = 
Collections.singletonList(new BusinessCalendarImpl.TimePeriod(startHoliday, 
endHoliday));
+        List<Integer> weekendDays = Collections.emptyList();
+        Calendar calendar = Calendar.getInstance();
+        int currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
+        BusinessCalendarImpl.rollCalendarAfterHolidays(calendar, holidays, 
weekendDays, false);
+        int expected = currentDayOfYear + holidayLeft + 1;
+        assertThat(calendar.get(Calendar.DAY_OF_YEAR)).isEqualTo(expected);
     }
 
     @Test
-    public void testCalculateMinutesBeforeStartHour() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "4");
-        config.setProperty(BusinessCalendarImpl.START_HOUR, "14");
-        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
-        String currentDate = "2024-11-28 10:48:33.000";
-        String duration = "10m";
-        String expectedDate = "2024-11-28 14:10:00";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
+    void rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking() {
+        List<Integer> workingDays = IntStream.range(Calendar.MONDAY, 
Calendar.SATURDAY).boxed().toList();
+        List<Integer> weekendDays = Arrays.asList(Calendar.SATURDAY, 
Calendar.SUNDAY);
+        boolean resetTime = false;
+        workingDays.forEach(workingDay -> {
+            Calendar calendar = getCalendarAtExpectedWeekDay(workingDay);
+            
BusinessCalendarImpl.rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar,
 weekendDays, resetTime);
+            
assertThat(calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(workingDay);
+        });
+        weekendDays.forEach(weekendDay -> {
+            Calendar calendar = getCalendarAtExpectedWeekDay(weekendDay);
+            
BusinessCalendarImpl.rollCalendarToNextWorkingDayIfCurrentDayIsNonWorking(calendar,
 weekendDays, resetTime);
+            
assertThat(calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(Calendar.MONDAY);
+        });
     }
 
     @Test
-    public void testCalculateSecondsBeforeStartHour() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "4");
-        config.setProperty(BusinessCalendarImpl.START_HOUR, "14");
-        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
-        String currentDate = "2024-11-28 10:48:33.000";
-        String duration = "10s";
-        String expectedDate = "2024-11-28 14:00:10";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
-    }
-
-    @Test
-    public void testCalculateMinutesBeforeEndHour() {
-        Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "4");
-        config.setProperty(BusinessCalendarImpl.START_HOUR, "14");
-        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
-        String currentDate = "2024-11-28 17:58:33.000";
-        String duration = "10m";
-        String expectedDate = "2024-11-29 14:08:33";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
-    }
+    void isWorkingDay() {
+        List<Integer> workingDays = IntStream.range(Calendar.MONDAY, 
Calendar.SATURDAY).boxed().toList();
+        List<Integer> weekendDays = Arrays.asList(Calendar.SATURDAY, 
Calendar.SUNDAY);
+        workingDays.forEach(workingDay -> 
assertThat(BusinessCalendarImpl.isWorkingDay(weekendDays, 
workingDay)).isTrue());
+        weekendDays.forEach(workingDay -> 
assertThat(BusinessCalendarImpl.isWorkingDay(weekendDays, 
workingDay)).isFalse());
+    }
+
+    private void commonCalculateBusinessTimeAsDateAssertBetweenHours(int 
startHourGap, int endHourGap, int testingCalendarHourGap, int 
executionHourDelay, int daysToSkip, String holidayDateFormat,
+            String holidays) {
+        BiFunction<Instant, Instant, Boolean> startBooleanCondition = 
(resultInstant, expectedStartTime) -> {
+            logger.debug("Check if {} is after or equal to {} ", 
resultInstant, expectedStartTime);
+            return !resultInstant.isBefore(expectedStartTime);
+        };
+        commonCalculateBusinessTimeAsDate(startHourGap,
+                endHourGap,
+                testingCalendarHourGap,
+                executionHourDelay,
+                daysToSkip,
+                holidayDateFormat,
+                holidays,
+                startBooleanCondition);
+    }
+
+    private void commonCalculateBusinessTimeAsDateAssertAtStartHour(int 
startHourGap, int endHourGap, int testingCalendarHourGap, int 
executionHourDelay, int daysToSkip, String holidayDateFormat,
+            String holidays) {
+        BiFunction<Instant, Instant, Boolean> startBooleanCondition = 
(resultInstant, expectedStartTime) -> {
+            logger.debug("Check if {} is equal to {} ", resultInstant, 
expectedStartTime);
+            return resultInstant.getEpochSecond() == 
expectedStartTime.getEpochSecond();
+        };
+        commonCalculateBusinessTimeAsDate(startHourGap,
+                endHourGap,
+                testingCalendarHourGap,
+                executionHourDelay,
+                daysToSkip,
+                holidayDateFormat,
+                holidays,
+                startBooleanCondition);
+    }
+
+    private void commonCalculateBusinessTimeAsDate(int startHourGap,
+            int endHourGap, int testingCalendarHourGap,
+            int executionHourDelay, int daysToSkip, String holidayDateFormat, 
String holidays,
+            BiFunction<Instant, Instant, Boolean> startBooleanCondition) {
+        logger.debug("startHourGap {}", startHourGap);
+        logger.debug("endHourGap {}", endHourGap);
+        logger.debug("testingCalendarHourGap {}", testingCalendarHourGap);
+        logger.debug("executionHourDelay {}", executionHourDelay);
+        logger.debug("numberOfHolidays {}", daysToSkip);
+        logger.debug("holidayDateFormat {}", holidayDateFormat);
+        logger.debug("holidays {}", holidays);
+
+        // lets pretend 12.00 is the current time
+        Calendar testingCalendar = Calendar.getInstance();
+        testingCalendar.set(Calendar.HOUR_OF_DAY, 12);
+        testingCalendar.set(Calendar.MINUTE, 0);
+        testingCalendar.set(Calendar.SECOND, 0);
+        logger.debug("testingCalendar {}", testingCalendar.getTime());
+        Calendar startCalendar = (Calendar) testingCalendar.clone();
+        startCalendar.add(Calendar.HOUR_OF_DAY, startHourGap);
+        logger.debug("startCalendar {}", startCalendar.getTime());
+        Calendar endCalendar = (Calendar) testingCalendar.clone();
+        endCalendar.add(Calendar.HOUR_OF_DAY, endHourGap);
+        logger.debug("endCalendar {}", endCalendar.getTime());
+
+        int startHour = startCalendar.get(Calendar.HOUR_OF_DAY);
+        int endHour = endCalendar.get(Calendar.HOUR_OF_DAY);
+
+        // We need to reconciliate for daily/working hours and daily/nightly 
hours
+        int hoursInDay = startHour < endHour ? endHour - startHour : 24 - 
(startHour - endHour);
+        int daysToAdd = daysToSkip;
+        logger.debug("daysToAdd (= numberOfHolidays) {}", daysToAdd);
+        if (executionHourDelay >= hoursInDay) {
+            daysToAdd += executionHourDelay / hoursInDay;
+            logger.debug("daysToAdd += (hourDelay / hoursInDay) {}", 
daysToAdd);
+        }
+        if (daysToAdd > 0) {
+            startCalendar.add(Calendar.DAY_OF_YEAR, daysToAdd);
+            endCalendar.add(Calendar.DAY_OF_YEAR, daysToAdd);
+            logger.debug("startCalendar (startCalendar + days to add) {}", 
startCalendar.getTime());
+            logger.debug("endCalendar (endCalendar + days to add) {}", 
endCalendar.getTime());
+        }
 
-    @Test
-    public void testCalculateSecondsBeforeEndHour() {
         Properties config = new Properties();
-        config.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, "4");
-        config.setProperty(BusinessCalendarImpl.START_HOUR, "14");
-        config.setProperty(BusinessCalendarImpl.END_HOUR, "18");
-        String currentDate = "2024-11-28 17:59:33.000";
-        String duration = "50s";
-        String expectedDate = "2024-11-29 14:00:23";
-
-        SessionPseudoClock clock = new 
StaticPseudoClock(parseToDateWithTimeAndMillis(currentDate).getTime());
-        BusinessCalendarImpl businessCal = new BusinessCalendarImpl(config, 
clock);
-
-        Date result = businessCal.calculateBusinessTimeAsDate(duration);
-
-        assertThat(formatDate("yyyy-MM-dd HH:mm:ss", 
result)).isEqualTo(expectedDate);
-    }
-
-    private Date parseToDate(String dateString) {
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-
-        Date testTime;
-        try {
-            testTime = sdf.parse(dateString);
-
-            return testTime;
-        } catch (ParseException e) {
-            return null;
+        config.setProperty(START_HOUR, String.valueOf(startHour));
+        config.setProperty(END_HOUR, String.valueOf(endHour));
+        config.setProperty(WEEKEND_DAYS, "0");
+        if (holidayDateFormat != null) {
+            config.setProperty(HOLIDAY_DATE_FORMAT, holidayDateFormat);
         }
-    }
-
-    private Date parseToDateWithTime(String dateString) {
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
-
-        Date testTime;
-        try {
-            testTime = sdf.parse(dateString);
-
-            return testTime;
-        } catch (ParseException e) {
-            return null;
+        if (holidays != null) {
+            config.setProperty(HOLIDAYS, holidays);
         }
-    }
 
-    private Date parseToDateWithTimeAndMillis(String dateString) {
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        testingCalendar.add(Calendar.HOUR_OF_DAY, testingCalendarHourGap);
+        logger.debug("testingCalendar after testingCalendarHourGap {}", 
testingCalendar.getTime());
+        BusinessCalendarImpl businessCal = 
BusinessCalendarImpl.builder().withCalendarBean(new CalendarBean(config))
+                .withTestingCalendar(testingCalendar)
+                .build();
+        Date retrieved = 
businessCal.calculateBusinessTimeAsDate(String.format("%sh", 
executionHourDelay));
+        logger.debug("retrieved {}", retrieved);
 
-        Date testTime;
-        try {
-            testTime = sdf.parse(dateString);
+        Date expectedStart = startCalendar.getTime();
+        Date expectedEnd = endCalendar.getTime();
 
-            return testTime;
-        } catch (ParseException e) {
-            return null;
-        }
-    }
+        Instant retrievedInstant = retrieved.toInstant();
+        Instant expectedStartTime = expectedStart.toInstant();
+        Instant expectedEndTime = expectedEnd.toInstant();
 
-    private String formatDate(String pattern, Date date) {
-        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
-
-        String testTime = sdf.format(date);
-
-        return testTime;
+        logger.debug("retrievedInstant {}", retrievedInstant);
+        logger.debug("expectedStartTime {}", expectedStartTime);
+        logger.debug("expectedEndTime {}", expectedEndTime);
 
+        assertThat(startBooleanCondition.apply(retrievedInstant, 
expectedStartTime)).isTrue();
+        logger.debug("Check if {} is not after {} ", retrievedInstant, 
expectedEndTime);
+        assertThat(retrievedInstant.isAfter(expectedEndTime)).isFalse();
     }
 
-    private class StaticPseudoClock implements SessionPseudoClock {
-
-        private long currentTime;
-
-        private StaticPseudoClock(long currenttime) {
-            this.currentTime = currenttime;
-        }
-
-        public long getCurrentTime() {
-            return this.currentTime;
-        }
-
-        public long advanceTime(long amount, TimeUnit unit) {
-            throw new UnsupportedOperationException("It is static clock and 
does not allow advance time operation");
+    private Calendar getCalendarAtExpectedWeekDay(int weekDay) {
+        Calendar toReturn = Calendar.getInstance();
+        while (toReturn.get(Calendar.DAY_OF_WEEK) != weekDay) {
+            toReturn.add(Calendar.DAY_OF_YEAR, 1);
         }
-
+        return toReturn;
     }
+
 }
diff --git 
a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanFactoryTest.java
 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanFactoryTest.java
new file mode 100644
index 0000000000..0a4f59d59f
--- /dev/null
+++ 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanFactoryTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.jbpm.process.core.timer;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CalendarBeanFactoryTest {
+
+    @Test
+    void testCreateCalendarBean() {
+        // This test relies on src/test/resources/calendar.properties:
+        // checked values comes from it
+        CalendarBean calendarBean = CalendarBeanFactory.createCalendarBean();
+        assertThat(calendarBean).isNotNull();
+        assertThat(calendarBean.getStartHour()).isEqualTo(10);
+        assertThat(calendarBean.getEndHour()).isEqualTo(16);
+        assertThat(calendarBean.getHoursInDay()).isEqualTo(6);
+        assertThat(calendarBean.getDaysPerWeek()).isEqualTo(5);
+        assertThat(calendarBean.getWeekendDays()).contains(Calendar.SATURDAY, 
Calendar.SUNDAY);
+        assertThat(calendarBean.getHolidays()).isEmpty();
+        
assertThat(calendarBean.getTimezone()).isEqualTo(TimeZone.getDefault().getID());
+    }
+}
diff --git 
a/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanTest.java
 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanTest.java
new file mode 100644
index 0000000000..64bf32194e
--- /dev/null
+++ 
b/jbpm/jbpm-flow/src/test/java/org/jbpm/process/core/timer/CalendarBeanTest.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.jbpm.process.core.timer;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.assertj.core.api.ThrowableAssert;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.END_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAYS;
+import static 
org.jbpm.process.core.timer.BusinessCalendarImpl.HOLIDAY_DATE_FORMAT;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.START_HOUR;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.TIMEZONE;
+import static org.jbpm.process.core.timer.BusinessCalendarImpl.WEEKEND_DAYS;
+import static 
org.jbpm.process.core.timer.CalendarBean.DEFAULT_HOLIDAY_DATE_FORMAT;
+import static org.jbpm.process.core.timer.CalendarBean.DEFAULT_TIMEZONE;
+import static org.jbpm.process.core.timer.CalendarBean.DEFAULT_WEEKENDS;
+import static org.jbpm.process.core.timer.CalendarBean.DEFAULT_WEEKEND_DAYS;
+
+class CalendarBeanTest {
+
+    // Static validation methods
+    @ParameterizedTest
+    @MethodSource("getMissingPropertiesCalendar")
+    void requiredPropertyValidation(Map<String, Object> propertyMap, 
List<String> errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonStaticMethodValidation(errorMessage -> 
CalendarBean.requiredPropertyValidation(errorMessage, calendarConfiguration), 
errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getWronglyFormatPropertiesCalendar")
+    void propertyFormatValidation(Map<String, Object> propertyMap, 
List<String> errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonStaticMethodValidation(errorMessage -> 
CalendarBean.propertyFormatValidation(errorMessage, calendarConfiguration), 
errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getBusinessInvalidPropertiesCalendar")
+    void businessValidation(Map<String, Object> propertyMap, List<String> 
errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonStaticMethodValidation(errorMessage -> 
CalendarBean.businessValidation(errorMessage, calendarConfiguration), 
errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getPartialPropertiesCalendar")
+    void missingDataPopulation(Map<String, Object> propertyMap, Map<String, 
Object> defaultValuesMap) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        defaultValuesMap.keySet().forEach(key -> 
assertThat(calendarConfiguration.containsKey(key)).isFalse());
+        CalendarBean.missingDataPopulation(calendarConfiguration);
+        defaultValuesMap.forEach((key, value) -> {
+            assertThat(calendarConfiguration.containsKey(key)).isTrue();
+            
assertThat(calendarConfiguration.getProperty(key)).isEqualTo(value);
+        });
+    }
+
+    @Test
+    void getPropertyAsInt() {
+        Properties calendarConfiguration = new Properties();
+        String propertyName = "propertyName";
+        int originalValue = 1;
+        String value = "" + originalValue;
+        calendarConfiguration.put(propertyName, value);
+        int retrieved = CalendarBean.getPropertyAsInt(propertyName, 
calendarConfiguration);
+        assertThat(retrieved).isEqualTo(originalValue);
+        value = "WRONG";
+        calendarConfiguration.put(propertyName, value);
+        String expectedMessage = "For input string: \"WRONG\"";
+        assertThatThrownBy(() -> CalendarBean.getPropertyAsInt(propertyName, 
calendarConfiguration))
+                .isInstanceOf(NumberFormatException.class)
+                .hasMessage(expectedMessage);
+    }
+
+    @Test
+    void validateRequiredProperty() {
+        Properties calendarConfiguration = new Properties();
+        String propertyName = "propertyName";
+        String value = "propertyValue";
+        calendarConfiguration.put(propertyName, value);
+        StringBuilder errorMessage = new StringBuilder();
+        CalendarBean.validateRequiredProperty(propertyName, errorMessage, 
calendarConfiguration);
+        assertThat(errorMessage).isEmpty();
+        CalendarBean.validateRequiredProperty("missingProperty", errorMessage, 
calendarConfiguration);
+        String[] retrievedErrors = errorMessage.toString().split("\n");
+        assertThat(retrievedErrors).hasSize(1);
+        assertThat(retrievedErrors).contains("Property missingProperty is 
required");
+    }
+
+    @Test
+    void getFormattedDate() throws ParseException {
+        Properties calendarConfiguration = new Properties();
+        String dateFormat = "dd-MM-yyyy";
+        String date = "27-11-2024";
+        calendarConfiguration.put(HOLIDAY_DATE_FORMAT, dateFormat);
+        Date retrieved = CalendarBean.getFormattedDate(date, 
calendarConfiguration);
+        Date expected = 
CalendarBean.getSimpleDateFormat(dateFormat).parse(date);
+        assertThat(retrieved).isEqualTo(expected);
+
+    }
+
+    @Test
+    void getSimpleDateFormat() {
+        SimpleDateFormat retrieved = 
CalendarBean.getSimpleDateFormat(DEFAULT_HOLIDAY_DATE_FORMAT);
+        assertThat(retrieved).isNotNull();
+        retrieved = CalendarBean.getSimpleDateFormat("dd-MM-yyyy");
+        assertThat(retrieved).isNotNull();
+        String wrong = "WRONG";
+        String expectedMessage = "Illegal pattern character 'R'";
+        assertThatThrownBy(() -> CalendarBean.getSimpleDateFormat(wrong))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage(expectedMessage);
+    }
+
+    // Instance methods
+    @ParameterizedTest
+    @MethodSource("getMissingPropertiesCalendar")
+    void requiredPropertyMissing(Map<String, Object> propertyMap, List<String> 
errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonIllegalArgumentAssertion(() -> new 
CalendarBean(calendarConfiguration), errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getWronglyFormatPropertiesCalendar")
+    void propertyWrongFormat(Map<String, Object> propertyMap, List<String> 
errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonIllegalArgumentAssertion(() -> new 
CalendarBean(calendarConfiguration), errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getBusinessInvalidPropertiesCalendar")
+    void businessInvalid(Map<String, Object> propertyMap, List<String> 
errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonIllegalArgumentAssertion(() -> new 
CalendarBean(calendarConfiguration), errorMessages);
+    }
+
+    @Test
+    void instantiationFull() throws ParseException {
+        Properties calendarConfiguration = new Properties();
+        int startHour = 10;
+        int endHour = 16;
+        List<Integer> weekendDays = Arrays.asList(3, 4);
+        String dateFormat = "dd-MM-yyyy";
+        String timezone = "ACT";
+        String holidays = "27-11-2024";
+        calendarConfiguration.put(START_HOUR, String.valueOf(startHour));
+        calendarConfiguration.put(END_HOUR, String.valueOf(endHour));
+        calendarConfiguration.put(WEEKEND_DAYS, 
weekendDays.stream().map(String::valueOf).collect(Collectors.joining(",")));
+        calendarConfiguration.put(HOLIDAY_DATE_FORMAT, dateFormat);
+        calendarConfiguration.put(TIMEZONE, timezone);
+        calendarConfiguration.put(HOLIDAYS, holidays);
+        CalendarBean retrieved = new CalendarBean(calendarConfiguration);
+
+        Date from = CalendarBean.getFormattedDate(holidays, 
calendarConfiguration);
+        Date to = CalendarBean.getFormattedDate("28-11-2024", 
calendarConfiguration);
+        assertThat(retrieved.getHolidays()).isEqualTo(List.of(new 
BusinessCalendarImpl.TimePeriod(from, to)));
+        assertThat(retrieved.getWeekendDays()).isEqualTo(weekendDays);
+        assertThat(retrieved.getDaysPerWeek()).isEqualTo(7 - 
weekendDays.size());
+        assertThat(retrieved.getTimezone()).isEqualTo(timezone);
+        assertThat(retrieved.getStartHour()).isEqualTo(startHour);
+        assertThat(retrieved.getEndHour()).isEqualTo(endHour);
+        assertThat(retrieved.getHoursInDay()).isEqualTo(endHour - startHour);
+    }
+
+    @Test
+    void instantiationPartial() {
+        Properties calendarConfiguration = new Properties();
+        int startHour = 10;
+        int endHour = 16;
+        calendarConfiguration.put(START_HOUR, String.valueOf(startHour));
+        calendarConfiguration.put(END_HOUR, String.valueOf(endHour));
+        CalendarBean retrieved = new CalendarBean(calendarConfiguration);
+        assertThat(retrieved.getHolidays()).isEqualTo(Collections.emptyList());
+        assertThat(retrieved.getWeekendDays()).isEqualTo(DEFAULT_WEEKEND_DAYS);
+        assertThat(retrieved.getDaysPerWeek()).isEqualTo(5);
+        assertThat(retrieved.getTimezone()).isEqualTo(DEFAULT_TIMEZONE);
+        assertThat(retrieved.getStartHour()).isEqualTo(startHour);
+        assertThat(retrieved.getEndHour()).isEqualTo(endHour);
+        assertThat(retrieved.getHoursInDay()).isEqualTo(endHour - startHour);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getMissingPropertiesCalendar")
+    void missingProperties(Map<String, Object> propertyMap, List<String> 
errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonIllegalArgumentAssertion(() -> new 
CalendarBean(calendarConfiguration), errorMessages);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getInvalidPropertiesCalendar")
+    public void invalidProperties(Map<String, Object> propertyMap, 
List<String> errorMessages) {
+        Properties calendarConfiguration = new Properties();
+        calendarConfiguration.putAll(propertyMap);
+        commonIllegalArgumentAssertion(() -> new 
CalendarBean(calendarConfiguration), errorMessages);
+    }
+
+    // Let's avoid duplication
+    private void commonStaticMethodValidation(Consumer<StringBuilder> 
executedMethod,
+            List<String> errorMessages) {
+        StringBuilder errors = new StringBuilder();
+        assertThat(errors).isEmpty();
+        executedMethod.accept(errors);
+        assertThat(errors).isNotEmpty();
+        String[] retrievedErrors = errors.toString().split("\n");
+        assertThat(retrievedErrors).hasSize(errorMessages.size());
+        errorMessages.forEach(msg -> 
assertThat(retrievedErrors).contains(msg));
+    }
+
+    private void 
commonIllegalArgumentAssertion(ThrowableAssert.ThrowingCallable executedMethod, 
List<String> errorMessages) {
+        ThrowableAssert throwableAssert = (ThrowableAssert) 
assertThatThrownBy(executedMethod)
+                .isInstanceOf(IllegalArgumentException.class);
+        errorMessages.forEach(throwableAssert::hasMessageContaining);
+    }
+
+    private static Stream<Arguments> getMissingPropertiesCalendar() {
+        return Stream.of(
+                Arguments.of(Map.of(), List.of("Property " + START_HOUR + " is 
required", "Property " + END_HOUR + " is required")),
+                Arguments.of(Map.of(START_HOUR, "9"), List.of("Property " + 
END_HOUR + " is required")),
+                Arguments.of(Map.of(END_HOUR, "17"), List.of("Property " + 
START_HOUR + " is required")));
+    }
+
+    private static Stream<Arguments> getWronglyFormatPropertiesCalendar() {
+
+        return Stream.of(
+                Arguments.of(Map.of(START_HOUR, "9", END_HOUR, "25"), 
List.of(END_HOUR + " 25 outside expected boundaries (0-24)")),
+                Arguments.of(Map.of(START_HOUR, "26", END_HOUR, "-2"), 
List.of(START_HOUR + " 26 outside expected boundaries (0-24)", END_HOUR + " -2 
outside expected boundaries (0-24)")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "1,2,8,9"), List.of(WEEKEND_DAYS + " [8, 9] outside expected 
boundaries (0-7)")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "0,1,2"), List.of("0 (= no weekends) and other values provided in 
the given " + WEEKEND_DAYS + " 0,1,2")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "1,1,2"), List.of("There are repeated values in the given " + 
WEEKEND_DAYS + " 1,1,2")),
+                Arguments.of(Map.of(START_HOUR, "", END_HOUR, ""), 
List.of(START_HOUR + " is not valid: For input string: \"\"", END_HOUR + " is 
not valid: For input string: \"\"")));
+    }
+
+    private static Stream<Arguments> getBusinessInvalidPropertiesCalendar() {
+
+        return Stream.of(
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "10"), 
List.of(START_HOUR + " 10 and " + END_HOUR + " 10 must be different")));
+    }
+
+    private static Stream<Arguments> getPartialPropertiesCalendar() {
+
+        return Stream.of(
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
HOLIDAY_DATE_FORMAT, "dd-mm-YYYY", TIMEZONE, "ACT"), Map.of(WEEKEND_DAYS, 
DEFAULT_WEEKENDS)),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "5,6", TIMEZONE, "ACT"), Map.of(HOLIDAY_DATE_FORMAT, 
DEFAULT_HOLIDAY_DATE_FORMAT)),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "5,6", HOLIDAY_DATE_FORMAT, "dd-mm-YYYY"), Map.of(TIMEZONE, 
DEFAULT_TIMEZONE)));
+    }
+
+    private static Stream<Arguments> getInvalidPropertiesCalendar() {
+
+        return Stream.of(
+                Arguments.of(Map.of(START_HOUR, "9", END_HOUR, "25"), 
List.of(END_HOUR + " 25 outside expected boundaries (0-24)")),
+                Arguments.of(Map.of(START_HOUR, "26", END_HOUR, "-2"), 
List.of(START_HOUR + " 26 outside expected boundaries (0-24)", END_HOUR + " -2 
outside expected boundaries (0-24)")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "1,2,8,9"), List.of(WEEKEND_DAYS + " [8, 9] outside expected 
boundaries (0-7)")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "10"), 
List.of(START_HOUR + " 10 and " + END_HOUR + " 10 must be different")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "0,1,2"), List.of("0 (= no weekends) and other values provided in 
the given " + WEEKEND_DAYS + " 0,1,2")),
+                Arguments.of(Map.of(START_HOUR, "10", END_HOUR, "4", 
WEEKEND_DAYS, "1,1,2"), List.of("There are repeated values in the given " + 
WEEKEND_DAYS + " 1,1,2")),
+                Arguments.of(Map.of(START_HOUR, "", END_HOUR, ""), 
List.of(START_HOUR + " is not valid: For input string: \"\"", END_HOUR + " is 
not valid: For input string: \"\"")));
+    }
+}
diff --git a/jbpm/jbpm-flow/src/test/resources/calendar.properties 
b/jbpm/jbpm-flow/src/test/resources/calendar.properties
new file mode 100644
index 0000000000..9acb81c21e
--- /dev/null
+++ b/jbpm/jbpm-flow/src/test/resources/calendar.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+business.end.hour=16
+business.start.hour=10
+
diff --git a/jbpm/jbpm-flow/src/test/resources/logback-test.xml 
b/jbpm/jbpm-flow/src/test/resources/logback-test.xml
index e269349389..9a48cd358d 100755
--- a/jbpm/jbpm-flow/src/test/resources/logback-test.xml
+++ b/jbpm/jbpm-flow/src/test/resources/logback-test.xml
@@ -29,6 +29,7 @@
   </appender>
 
   <logger name="org.jbpm" level="info"/>
+  <logger name="org.jbpm.process.core.timer" level="info"/>
   <logger name="com.arjuna" level="error"/>
   <logger name="org.hibernate" level="warn"/>
  
diff --git 
a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java
 
b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTimerProcessTest.java
similarity index 82%
rename from 
jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java
rename to 
jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTimerProcessTest.java
index 0ca0a9bde8..99704a6e18 100644
--- 
a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTest.java
+++ 
b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/calendar/BusinessCalendarTimerProcessTest.java
@@ -28,6 +28,7 @@ import java.util.Properties;
 
 import org.jbpm.bpmn2.objects.TestWorkItemHandler;
 import org.jbpm.process.core.timer.BusinessCalendarImpl;
+import org.jbpm.process.core.timer.CalendarBean;
 import org.jbpm.test.utils.ProcessTestHelper;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -38,19 +39,20 @@ import org.kie.kogito.process.impl.AbstractProcessConfig;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-public class BusinessCalendarTest {
+public class BusinessCalendarTimerProcessTest {
 
-    private static BusinessCalendar workingDayCalendar;
-    private static BusinessCalendar notWorkingDayCalendar;
+    private static Properties notWorkingDayCalendarConfiguration;
+    private static Properties workingDayCalendarConfiguration;
 
     @BeforeAll
     public static void createCalendars() {
-        workingDayCalendar = configureBusinessCalendar(true);
-        notWorkingDayCalendar = configureBusinessCalendar(false);
+        workingDayCalendarConfiguration = configureBusinessCalendar(true);
+        notWorkingDayCalendarConfiguration = configureBusinessCalendar(false);
     }
 
     @Test
     public void testTimerWithWorkingDayCalendar() throws InterruptedException {
+        BusinessCalendar workingDayCalendar = 
BusinessCalendarImpl.builder().withCalendarBean(new 
CalendarBean(workingDayCalendarConfiguration)).build();
         Application app = ProcessTestHelper.newApplication(new 
MockProcessConfig(workingDayCalendar));
         TestWorkItemHandler workItemHandler = new TestWorkItemHandler();
         ProcessTestHelper.registerHandler(app, "Human Task", workItemHandler);
@@ -65,6 +67,7 @@ public class BusinessCalendarTest {
 
     @Test
     public void testTimerWithNotWorkingDayCalendar() throws 
InterruptedException {
+        BusinessCalendar notWorkingDayCalendar = 
BusinessCalendarImpl.builder().withCalendarBean(new 
CalendarBean(notWorkingDayCalendarConfiguration)).build();
         Application app = ProcessTestHelper.newApplication(new 
MockProcessConfig(notWorkingDayCalendar));
         TestWorkItemHandler workItemHandler = new TestWorkItemHandler();
         ProcessTestHelper.registerHandler(app, "Human Task", workItemHandler);
@@ -77,14 +80,12 @@ public class BusinessCalendarTest {
         assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE);
     }
 
-    private static BusinessCalendar configureBusinessCalendar(boolean 
isWorkingDayCalendar) {
+    private static Properties configureBusinessCalendar(boolean 
isWorkingDayCalendar) {
         Properties businessCalendarConfiguration = new Properties();
         if (isWorkingDayCalendar) {
             
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.START_HOUR, "0");
             
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.END_HOUR, "24");
-            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOURS_PER_DAY, 
"24");
-            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.DAYS_PER_WEEK, 
"7");
-            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, 
"8,9");
+            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, 
"0");
         } else {
             Calendar currentCalendar = Calendar.getInstance();
             Date today = new Date();
@@ -92,10 +93,13 @@ public class BusinessCalendarTest {
             Date tomorrow = currentCalendar.getTime();
             String dateFormat = "yyyy-MM-dd";
             SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
+            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.START_HOUR, "9");
+            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.END_HOUR, "17");
             
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOLIDAYS, 
sdf.format(today) + "," + sdf.format(tomorrow));
+            
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.WEEKEND_DAYS, 
"1,2,3,4,5");
             
businessCalendarConfiguration.setProperty(BusinessCalendarImpl.HOLIDAY_DATE_FORMAT,
 dateFormat);
         }
-        return new BusinessCalendarImpl(businessCalendarConfiguration);
+        return businessCalendarConfiguration;
     }
 
     private static class MockProcessConfig extends AbstractProcessConfig {
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
index 4504af36e1..ff375187a2 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
@@ -31,6 +31,6 @@ public class BusinessCalendarProducer {
 
     @Produces
     public BusinessCalendar createBusinessCalendar() {
-        return new BusinessCalendarImpl();
+        return BusinessCalendarImpl.builder().build();
     }
 }
\ No newline at end of file
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
index 8fd9f766a1..2f0ca351f8 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
@@ -33,6 +33,6 @@ public class BusinessCalendarProducer {
     @Bean
     public BusinessCalendar createBusinessCalendar() {
 
-        return new BusinessCalendarImpl();
+        return BusinessCalendarImpl.builder().build();
     }
 }
\ No newline at end of file
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
deleted file mode 100644
index dd5e5f18ea..0000000000
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerQuarkusTemplate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package $Package$;
-
-import org.kie.kogito.calendar.BusinessCalendar;
-import org.kie.kogito.calendar.BusinessCalendarImpl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import jakarta.enterprise.inject.Produces;
-
-public class BusinessCalendarProducer {
-
-    private static final Logger logger = 
LoggerFactory.getLogger(BusinessCalendarProducer.class);
-
-    @Produces
-    public BusinessCalendar createBusinessCalendar() {
-        return new BusinessCalendarImpl();
-    }
-}
\ No newline at end of file
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
deleted file mode 100644
index e80c7cb10b..0000000000
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/class-templates/producer/BusinessCalendarProducerSpringTemplate.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package $Package$;
-
-import org.kie.kogito.calendar.BusinessCalendar;
-import org.kie.kogito.calendar.BusinessCalendarImpl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class BusinessCalendarProducer {
-
-    private static final Logger logger = 
LoggerFactory.getLogger(BusinessCalendarProducer.class);
-
-    @Bean
-    public BusinessCalendar createBusinessCalendar() {
-        return new BusinessCalendarImpl();
-    }
-}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to