content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * Licensed 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.activiti.engine.test.bpmn.event.timer;
  
  import static org.assertj.core.api.Assertions.assertThat;
  
  import java.text.SimpleDateFormat;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import org.activiti.engine.delegate.DelegateExecution;
  import org.activiti.engine.delegate.ExecutionListener;
  import org.activiti.engine.impl.test.PluggableActivitiTestCase;
  import org.activiti.engine.runtime.Job;
  import org.activiti.engine.runtime.ProcessInstance;
  import org.activiti.engine.runtime.TimerJobQuery;
  import org.activiti.engine.task.Task;
  import org.activiti.engine.test.Deployment;
  
  /**
  
   */
  public class BoundaryTimerEventTest extends PluggableActivitiTestCase {
  
      private static boolean listenerExecutedStartEvent;
      private static boolean listenerExecutedEndEvent;
  
      public static class MyExecutionListener implements ExecutionListener {
  
          private static final long serialVersionUID = 1L;
  
          public void notify(DelegateExecution execution) {
              if ("end".equals(execution.getEventName())) {
                  listenerExecutedEndEvent = true;
              } else if ("start".equals(execution.getEventName())) {
                  listenerExecutedStartEvent = true;
              }
          }
      }
  
      @Override
      protected void setUp() throws Exception {
          super.setUp();
          listenerExecutedStartEvent = false;
          listenerExecutedEndEvent = false;
      }
  
      /*
       * Test for when multiple boundary timer events are defined on the same user task
       *
       * Configuration: - timer 1 -> 2 hours -> secondTask - timer 2 -> 1 hour -> thirdTask - timer 3 -> 3 hours -> fourthTask
       *
       * See process image next to the process xml resource
       */
      @Deployment
      public void testMultipleTimersOnUserTask() {
          // Set the clock fixed
          Date startTime = new Date();
  
          // After process start, there should be 3 timers created
          ProcessInstance pi = runtimeService.startProcessInstanceByKey("multipleTimersOnUserTask");
          TimerJobQuery jobQuery = managementService.createTimerJobQuery().processInstanceId(pi.getId());
          List<Job> jobs = jobQuery.list();
          assertThat(jobs).hasSize(3);
  
          // After setting the clock to time '1 hour and 5 seconds', the second
          // timer should fire
          processEngineConfiguration.getClock().setCurrentTime(new Date(startTime.getTime() + ((60 * 60 * 1000) + 5000)));
          waitForJobExecutorToProcessAllJobs(5000L, 25L);
          assertThat(jobQuery.count()).isEqualTo(0L);
  
          // which means that the third task is reached
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task.getName()).isEqualTo("Third Task");
      }
  
      @Deployment
      public void testTimerOnNestingOfSubprocesses() {
          Date testStartTime = processEngineConfiguration.getClock().getCurrentTime();
  
          runtimeService.startProcessInstanceByKey("timerOnNestedSubprocesses");
          List<Task> tasks = taskService.createTaskQuery().orderByTaskName().asc().list();
          assertThat(tasks).hasSize(2);
          assertThat(tasks.get(0).getName()).isEqualTo("Inner subprocess task 1");
          assertThat(tasks.get(1).getName()).isEqualTo("Inner subprocess task 2");
  
          // Timer will fire in 2 hours
          processEngineConfiguration
              .getClock()
              .setCurrentTime(new Date(testStartTime.getTime() + ((2 * 60 * 60 * 1000) + 5000)));
          Job timer = managementService.createTimerJobQuery().singleResult();
          managementService.moveTimerToExecutableJob(timer.getId());
          managementService.executeJob(timer.getId());
  
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task.getName()).isEqualTo("task outside subprocess");
      }
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/event/timer/BoundaryTimerEventTest.testTimerOnUserTask.bpmn20.xml",
          }
      )
      public void testExpressionOnTimerWithExecutionListeners_completeUserTask() {
          // Set the clock fixed
          Date startTime = new Date();
  
          HashMap<String, Object> variables = new HashMap<String, Object>();
          variables.put("duration", "PT1H");
  
          // After process start, there should be a timer created
          ProcessInstance pi = runtimeService.startProcessInstanceByKey(
              "testExpressionOnTimerWithExecutionListeners",
              variables
          );
  
          TimerJobQuery jobQuery = managementService.createTimerJobQuery().processInstanceId(pi.getId());
          List<Job> jobs = jobQuery.list();
          assertThat(jobs).hasSize(1);
  
          Task task = taskService.createTaskQuery().singleResult();
          taskService.complete(task.getId());
  
          // After setting the clock to time '1 hour and 5 seconds', the second
          // timer should fire
          processEngineConfiguration.getClock().setCurrentTime(new Date(startTime.getTime() + ((60 * 60 * 1000) + 5000)));
          waitForJobExecutorToProcessAllJobs(5000L, 25L);
          assertThat(jobQuery.count()).isEqualTo(0L);
  
          // start execution listener is not executed
          assertThat(listenerExecutedStartEvent).isFalse();
          assertThat(listenerExecutedEndEvent).isTrue();
  
          // which means the process has ended
          assertProcessEnded(pi.getId());
      }
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/event/timer/BoundaryTimerEventTest.testTimerOnUserTask.bpmn20.xml",
          }
      )
      public void testExpressionOnTimerWithExecutionListeners_expiredTimerEvent() {
          // Set the clock fixed
          Date startTime = new Date();
  
          HashMap<String, Object> variables = new HashMap<String, Object>();
          variables.put("duration", "PT1H");
  
          // After process start, there should be a timer created
          ProcessInstance pi = runtimeService.startProcessInstanceByKey(
              "testExpressionOnTimerWithExecutionListeners",
              variables
          );
  
          TimerJobQuery jobQuery = managementService.createTimerJobQuery().processInstanceId(pi.getId());
          List<Job> jobs = jobQuery.list();
          assertThat(jobs).hasSize(1);
  
          // After setting the clock to time '1 hour and 5 seconds', the second
          // timer should fire
          processEngineConfiguration.getClock().setCurrentTime(new Date(startTime.getTime() + ((60 * 60 * 1000) + 5000)));
          waitForJobExecutorToProcessAllJobs(5000L, 25L);
          assertThat(jobQuery.count()).isEqualTo(0L);
  
          // start execution listener is not executed
          assertThat(listenerExecutedStartEvent).isFalse();
          assertThat(listenerExecutedEndEvent).isTrue();
  
          // which means the process has ended
          assertProcessEnded(pi.getId());
      }
  
      @Deployment
      public void testExpressionOnTimer() {
          // Set the clock fixed
          Date startTime = new Date();
  
          HashMap<String, Object> variables = new HashMap<String, Object>();
          variables.put("duration", "PT1H");
  
          // After process start, there should be a timer created
          ProcessInstance pi = runtimeService.startProcessInstanceByKey("testExpressionOnTimer", variables);
  
          TimerJobQuery jobQuery = managementService.createTimerJobQuery().processInstanceId(pi.getId());
          List<Job> jobs = jobQuery.list();
          assertThat(jobs).hasSize(1);
  
          // After setting the clock to time '1 hour and 5 seconds', the second
          // timer should fire
          processEngineConfiguration.getClock().setCurrentTime(new Date(startTime.getTime() + ((60 * 60 * 1000) + 5000)));
          waitForJobExecutorToProcessAllJobs(5000L, 25L);
          assertThat(jobQuery.count()).isEqualTo(0L);
  
          // start execution listener is not executed
          assertThat(listenerExecutedStartEvent).isFalse();
          assertThat(listenerExecutedEndEvent).isTrue();
  
          // which means the process has ended
          assertProcessEnded(pi.getId());
      }
  
      @Deployment
      public void testNullExpressionOnTimer() {
          HashMap<String, Object> variables = new HashMap<String, Object>();
          variables.put("duration", null);
  
          // After process start, there should be a timer created
          ProcessInstance pi = runtimeService.startProcessInstanceByKey("testNullExpressionOnTimer", variables);
  
          //NO job scheduled as null expression set
          TimerJobQuery jobQuery = managementService.createTimerJobQuery().processInstanceId(pi.getId());
          List<Job> jobs = jobQuery.list();
          assertThat(jobs).hasSize(0);
  
          // which means the process is still running waiting for human task input.
          ProcessInstance processInstance = processEngine
              .getRuntimeService()
              .createProcessInstanceQuery()
              .processInstanceId(pi.getId())
              .singleResult();
          assertThat(processInstance).isNotNull();
      }
  
      @Deployment
      public void testTimerInSingleTransactionProcess() {
          // make sure that if a PI completes in single transaction, JobEntities
          // associated with the execution are deleted.
          // broken before 5.10, see ACT-1133
          runtimeService.startProcessInstanceByKey("timerOnSubprocesses");
          assertThat(managementService.createJobQuery().count()).isEqualTo(0);
      }
  
      @Deployment
      public void testRepeatingTimerWithCancelActivity() {
          runtimeService.startProcessInstanceByKey("repeatingTimerAndCallActivity");
          assertThat(managementService.createTimerJobQuery().count()).isEqualTo(1);
          assertThat(taskService.createTaskQuery().count()).isEqualTo(1);
  
          // Firing job should cancel the user task, destroy the scope,
          // re-enter the task and recreate the task. A new timer should also be
          // created.
          // This didn't happen before 5.11 (new jobs kept being created). See
          // ACT-1427
          Job job = managementService.createTimerJobQuery().singleResult();
          managementService.moveTimerToExecutableJob(job.getId());
          managementService.executeJob(job.getId());
          assertThat(managementService.createTimerJobQuery().count()).isEqualTo(1);
          assertThat(taskService.createTaskQuery().count()).isEqualTo(1);
      }
  
      @Deployment
      public void testInfiniteRepeatingTimer() throws Exception {
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyy.MM.dd hh:mm");
          Date currentTime = simpleDateFormat.parse("2015.10.01 11:01");
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          Map<String, Object> vars = new HashMap<String, Object>();
          vars.put("timerString", "R/2015-10-01T11:00:00/PT24H");
          runtimeService.startProcessInstanceByKey("testTimerErrors", vars);
  
          long twentyFourHours = 24L * 60L * 60L * 1000L;
  
          Date previousDueDate = null;
  
          // Move clock, job should fire
          for (int i = 0; i < 30; i++) {
              Job job = managementService.createTimerJobQuery().singleResult();
  
              // Verify due date
              if (previousDueDate != null) {
                  assertThat(job.getDuedate().getTime() - previousDueDate.getTime() >= twentyFourHours).isTrue();
              }
              previousDueDate = job.getDuedate();
  
              currentTime = new Date(currentTime.getTime() + twentyFourHours + (60 * 1000));
              processEngineConfiguration.getClock().setCurrentTime(currentTime);
              String jobId = managementService.createTimerJobQuery().singleResult().getId();
              managementService.moveTimerToExecutableJob(jobId);
              managementService.executeJob(jobId);
          }
      }
  
      @Deployment
      public void testRepeatTimerDuration() throws Exception {
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyy.MM.dd hh:mm");
          Date currentTime = simpleDateFormat.parse("2015.10.01 11:01");
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          runtimeService.startProcessInstanceByKey("repeattimertest");
  
          long twentyFourHours = 24L * 60L * 60L * 1000L;
  
          Date previousDueDate = null;
  
          // Move clock, job should fire
          for (int i = 0; i < 3; i++) {
              Job job = managementService.createTimerJobQuery().singleResult();
  
              // Verify due date
              if (previousDueDate != null) {
                  assertThat(job.getDuedate().getTime() - previousDueDate.getTime() >= twentyFourHours).isTrue();
              }
              previousDueDate = job.getDuedate();
  
              currentTime = new Date(currentTime.getTime() + twentyFourHours + (60 * 1000));
              processEngineConfiguration.getClock().setCurrentTime(currentTime);
              managementService.moveTimerToExecutableJob(job.getId());
              managementService.executeJob(job.getId());
          }
      }
  
      @Deployment
      public void testBoundaryTimerEvent() throws Exception {
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyy.MM.dd hh:mm");
          Date currentTime = simpleDateFormat.parse("2015.10.01 11:01");
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          Map<String, Object> vars = new HashMap<String, Object>();
          vars.put("patient", "kermit");
          runtimeService.startProcessInstanceByKey("process1", vars);
  
          // just wait for 2 seconds to run any job if it's the case
          try {
              waitForJobExecutorToProcessAllJobsAndExecutableTimerJobs(2000, 200);
          } catch (Exception ex) {
              //expected exception because the boundary timer event created a timer job to be executed after 10 minutes
          }
  
          // there should be a userTask waiting for user input
          List<Task> tasks = taskService.createTaskQuery().list();
          assertThat(tasks).hasSize(1);
          assertThat(tasks.get(0).getName()).isEqualTo("First Task");
          List<Job> jobList = managementService.createTimerJobQuery().list();
          assertThat(jobList).hasSize(1);
  
          // let's see what's happening after 2 minutes
          // nothing should change since the timer have to executed after 10 minutes
          long twoMinutes = 2L * 60L * 1000L;
  
          currentTime = new Date(currentTime.getTime() + twoMinutes + 1000L);
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          try {
              waitForJobExecutorToProcessAllJobsAndExecutableTimerJobs(2000, 200);
          } catch (Exception ex) {
              //expected exception because the boundary timer event created a timer job to be executed after 10 minutes
          }
  
          tasks = taskService.createTaskQuery().list();
          assertThat(tasks).hasSize(1);
          assertThat(tasks.get(0).getName()).isEqualTo("First Task");
          jobList = managementService.createTimerJobQuery().list();
          assertThat(jobList).hasSize(1);
  
          // after another 8 minutes (the timer will have to execute because it wasa set to be executed @ 10 minutes after process start)
          long tenMinutes = 8L * 60L * 1000L;
          currentTime = new Date(currentTime.getTime() + tenMinutes);
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          try {
              waitForJobExecutorToProcessAllJobsAndExecutableTimerJobs(2000, 200);
          } catch (Exception ex) {
              ex.getCause();
              //expected exception because a new job is prepared
          }
  
          // there should be only one userTask and it should be the one triggered by the boundary timer event.
          // after the boundary event is triggered there should be no active job.
          tasks = taskService.createTaskQuery().list();
          assertThat(tasks).hasSize(1);
          assertThat(tasks.get(0).getName()).isEqualTo("Second Task");
          jobList = managementService.createJobQuery().list();
          assertThat(jobList).hasSize(0);
          jobList = managementService.createTimerJobQuery().list();
          assertThat(jobList).hasSize(0);
      }
  
      @Deployment
      public void testBoundaryTimerEvent2() throws Exception {
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyy.MM.dd hh:mm");
          Date currentTime = simpleDateFormat.parse("2015.10.01 11:01");
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          runtimeService.startProcessInstanceByKey("timerprocess");
  
          // just wait for 2 seconds to run any job if it's the case
          try {
              waitForJobExecutorToProcessAllJobsAndExecutableTimerJobs(2000, 200);
          } catch (Exception ex) {
              //expected exception because the boundary timer event created a timer job to be executed after 10 minutes
          }
  
          // there should be a userTask waiting for user input
          List<Task> tasks = taskService.createTaskQuery().list();
          assertThat(tasks).hasSize(1);
          assertThat(tasks.get(0).getName()).isEqualTo("Start");
          List<Job> jobList = managementService.createTimerJobQuery().list();
          assertThat(jobList).hasSize(1);
  
          // after another 2 minutes
          long tenMinutes = 2L * 60L * 1000L;
          currentTime = new Date(currentTime.getTime() + tenMinutes);
          processEngineConfiguration.getClock().setCurrentTime(currentTime);
  
          try {
              waitForJobExecutorToProcessAllJobsAndExecutableTimerJobs(2000, 200);
          } catch (Exception ex) {
              ex.getCause();
              //expected exception because a new job is prepared
          }
  
          // there should be no userTask
          tasks = taskService.createTaskQuery().list();
          assertThat(tasks).hasSize(0);
          jobList = managementService.createJobQuery().list();
          assertThat(jobList).hasSize(0);
          jobList = managementService.createTimerJobQuery().list();
          assertThat(jobList).hasSize(0);
      }
  }
  