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.spring.boot;
  
  import static java.util.Collections.emptyList;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Objects;
  import java.util.Optional;
  import javax.sql.DataSource;
  import org.activiti.api.process.model.events.ApplicationDeployedEvent;
  import org.activiti.api.process.model.events.ProcessDeployedEvent;
  import org.activiti.api.process.model.events.StartMessageDeployedEvent;
  import org.activiti.api.process.runtime.events.ProcessCandidateStarterGroupAddedEvent;
  import org.activiti.api.process.runtime.events.ProcessCandidateStarterUserAddedEvent;
  import org.activiti.api.process.runtime.events.listener.ProcessRuntimeEventListener;
  import org.activiti.api.runtime.shared.identity.UserGroupManager;
  import org.activiti.core.common.spring.project.ApplicationUpgradeContextService;
  import org.activiti.engine.ManagementService;
  import org.activiti.engine.RepositoryService;
  import org.activiti.engine.cfg.ProcessEngineConfigurator;
  import org.activiti.engine.impl.bpmn.behavior.VariablesPropagator;
  import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
  import org.activiti.engine.impl.event.EventSubscriptionPayloadMappingProvider;
  import org.activiti.engine.impl.persistence.StrongUuidGenerator;
  import org.activiti.engine.impl.persistence.deploy.DeploymentCache;
  import org.activiti.engine.impl.persistence.deploy.ProcessDefinitionCacheEntry;
  import org.activiti.runtime.api.event.impl.StartMessageSubscriptionConverter;
  import org.activiti.runtime.api.impl.ExtensionsVariablesMappingProvider;
  import org.activiti.runtime.api.model.impl.APIDeploymentConverter;
  import org.activiti.runtime.api.model.impl.APIProcessDefinitionConverter;
  import org.activiti.spring.ApplicationDeployedEventProducer;
  import org.activiti.spring.ProcessCandidateStartersEventProducer;
  import org.activiti.spring.ProcessDeployedEventProducer;
  import org.activiti.spring.SpringAsyncExecutor;
  import org.activiti.spring.SpringProcessEngineConfiguration;
  import org.activiti.spring.StartMessageDeployedEventProducer;
  import org.activiti.spring.boot.process.validation.AsyncPropertyValidator;
  import org.activiti.spring.cache.SpringProcessDefinitionCache;
  import org.activiti.spring.process.ProcessExtensionResourceFinderDescriptor;
  import org.activiti.spring.process.ProcessVariablesInitiator;
  import org.activiti.spring.resources.ResourceFinder;
  import org.activiti.spring.resources.ResourceFinderDescriptor;
  import org.activiti.validation.ProcessValidatorImpl;
  import org.activiti.validation.validator.ValidatorSet;
  import org.springframework.beans.factory.ObjectProvider;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.boot.autoconfigure.AutoConfiguration;
  import org.springframework.boot.autoconfigure.AutoConfigureAfter;
  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  import org.springframework.boot.context.properties.EnableConfigurationProperties;
  import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization;
  import org.springframework.cache.CacheManager;
  import org.springframework.context.ApplicationEventPublisher;
  import org.springframework.context.annotation.Bean;
  import org.springframework.core.Ordered;
  import org.springframework.core.annotation.Order;
  import org.springframework.core.io.Resource;
  import org.springframework.transaction.PlatformTransactionManager;
  
  @AutoConfiguration
  @AutoConfigureAfter(
      name = {
          "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
          "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration",
      }
  )
  @EnableConfigurationProperties({ ActivitiProperties.class, AsyncExecutorProperties.class })
  public class ProcessEngineAutoConfiguration extends AbstractProcessEngineAutoConfiguration {
  
      public static final String BEHAVIOR_FACTORY_MAPPING_CONFIGURER = "behaviorFactoryMappingConfigurer";
      private final UserGroupManager userGroupManager;
  
      public ProcessEngineAutoConfiguration(UserGroupManager userGroupManager) {
          this.userGroupManager = userGroupManager;
      }
  
      @Bean
      public ShutdownListener shutdownListener(ProcessEngineConfigurationImpl processEngineConfiguration) {
          return new ShutdownListener(processEngineConfiguration);
      }
  
      @Bean
      @ConditionalOnMissingBean
      @DependsOnDatabaseInitialization
      public SpringProcessEngineConfiguration springProcessEngineConfiguration(
          DataSource dataSource,
          PlatformTransactionManager transactionManager,
          SpringAsyncExecutor springAsyncExecutor,
          ActivitiProperties activitiProperties,
          ResourceFinder resourceFinder,
          List<ResourceFinderDescriptor> resourceFinderDescriptors,
          ApplicationUpgradeContextService applicationUpgradeContextService,
          @Autowired(required = false) List<ProcessEngineConfigurationConfigurer> processEngineConfigurationConfigurers,
          @Autowired(required = false) List<ProcessEngineConfigurator> processEngineConfigurators,
          ObjectProvider<DeploymentCache<ProcessDefinitionCacheEntry>> processDefinitionCacheProvider
      ) throws IOException {
          SpringProcessEngineConfiguration conf = new SpringProcessEngineConfiguration(applicationUpgradeContextService);
          conf.setConfigurators(processEngineConfigurators);
  
          processDefinitionCacheProvider.ifAvailable(conf::setProcessDefinitionCache);
  
          configureResources(resourceFinder, resourceFinderDescriptors, conf);
  
          conf.setDataSource(dataSource);
          conf.setTransactionManager(transactionManager);
  
          conf.setAsyncExecutor(springAsyncExecutor);
          conf.setDeploymentName(activitiProperties.getDeploymentName());
          conf.setDatabaseSchema(activitiProperties.getDatabaseSchema());
          conf.setDatabaseSchemaUpdate(activitiProperties.getDatabaseSchemaUpdate());
          conf.setDbHistoryUsed(activitiProperties.isDbHistoryUsed());
          conf.setAsyncExecutorActivate(activitiProperties.isAsyncExecutorActivate());
          addAsyncPropertyValidator(activitiProperties, conf);
          conf.setMailServerHost(activitiProperties.getMailServerHost());
          conf.setMailServerPort(activitiProperties.getMailServerPort());
          conf.setMailServerUsername(activitiProperties.getMailServerUserName());
          conf.setMailServerPassword(activitiProperties.getMailServerPassword());
          conf.setMailServerDefaultFrom(activitiProperties.getMailServerDefaultFrom());
          conf.setMailServerUseSSL(activitiProperties.isMailServerUseSsl());
          conf.setMailServerUseTLS(activitiProperties.isMailServerUseTls());
          conf.setDisableExistingStartEventSubscriptions(
              activitiProperties.shouldDisableExistingStartEventSubscriptions()
          );
  
          if (userGroupManager != null) {
              conf.setUserGroupManager(userGroupManager);
          }
  
          conf.setHistoryLevel(activitiProperties.getHistoryLevel());
          conf.setCopyVariablesToLocalForTasks(activitiProperties.isCopyVariablesToLocalForTasks());
          conf.setSerializePOJOsInVariablesToJson(activitiProperties.isSerializePOJOsInVariablesToJson());
          conf.setJavaClassFieldForJackson(activitiProperties.getJavaClassFieldForJackson());
  
          if (activitiProperties.getCustomMybatisMappers() != null) {
              conf.setCustomMybatisMappers(getCustomMybatisMapperClasses(activitiProperties.getCustomMybatisMappers()));
          }
  
          if (activitiProperties.getCustomMybatisXMLMappers() != null) {
              conf.setCustomMybatisXMLMappers(new HashSet<>(activitiProperties.getCustomMybatisXMLMappers()));
          }
  
          if (activitiProperties.getCustomMybatisXMLMappers() != null) {
              conf.setCustomMybatisXMLMappers(new HashSet<>(activitiProperties.getCustomMybatisXMLMappers()));
          }
  
          if (activitiProperties.isUseStrongUuids()) {
              conf.setIdGenerator(new StrongUuidGenerator());
          }
  
          if (activitiProperties.getDeploymentMode() != null) {
              conf.setDeploymentMode(activitiProperties.getDeploymentMode());
          }
  
          if (processEngineConfigurationConfigurers != null) {
              for (ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer : processEngineConfigurationConfigurers) {
                  processEngineConfigurationConfigurer.configure(conf);
              }
          }
  
          if (activitiProperties.getProcessDefinitionCacheLimit() != null) {
              conf.setProcessDefinitionCacheLimit(activitiProperties.getProcessDefinitionCacheLimit());
          }
  
          springAsyncExecutor.applyConfig(conf);
          return conf;
      }
  
      private void configureResources(
          ResourceFinder resourceFinder,
          List<ResourceFinderDescriptor> resourceFinderDescriptors,
          SpringProcessEngineConfiguration conf
      ) throws IOException {
          List<Resource> resources = new ArrayList<>();
          for (ResourceFinderDescriptor resourceFinderDescriptor : resourceFinderDescriptors) {
              resources.addAll(resourceFinder.discoverResources(resourceFinderDescriptor));
          }
  
          conf.setDeploymentResources(resources.toArray(new Resource[0]));
      }
  
      protected void addAsyncPropertyValidator(
          ActivitiProperties activitiProperties,
          SpringProcessEngineConfiguration conf
      ) {
          if (!activitiProperties.isAsyncExecutorActivate()) {
              ValidatorSet springBootStarterValidatorSet = new ValidatorSet("activiti-spring-boot-starter");
              springBootStarterValidatorSet.addValidator(new AsyncPropertyValidator());
              if (conf.getProcessValidator() == null) {
                  ProcessValidatorImpl processValidator = new ProcessValidatorImpl();
                  processValidator.addValidatorSet(springBootStarterValidatorSet);
                  conf.setProcessValidator(processValidator);
              } else {
                  conf.getProcessValidator().getValidatorSets().add(springBootStarterValidatorSet);
              }
          }
      }
  
      @Bean
      @ConditionalOnMissingBean
      public ProcessDefinitionResourceFinderDescriptor processDefinitionResourceFinderDescriptor(
          ActivitiProperties activitiProperties
      ) {
          return new ProcessDefinitionResourceFinderDescriptor(activitiProperties);
      }
  
      @Bean
      @ConditionalOnMissingBean
      public ProcessExtensionResourceFinderDescriptor processExtensionResourceFinderDescriptor(
          ActivitiProperties activitiProperties,
          @Value("${spring.activiti.process.extensions.dir:NOT_DEFINED}") String locationPrefix,
          @Value("${spring.activiti.process.extensions.suffix:**-extensions.json}") String locationSuffix
      ) {
          if (locationPrefix.equalsIgnoreCase("NOT_DEFINED")) locationPrefix =
              activitiProperties.getProcessDefinitionLocationPrefix();
          return new ProcessExtensionResourceFinderDescriptor(
              activitiProperties.isCheckProcessDefinitions(),
              locationPrefix,
              locationSuffix
          );
      }
  
      @Bean
      @ConditionalOnMissingBean
      public ProcessDeployedEventProducer processDeployedEventProducer(
          RepositoryService repositoryService,
          APIProcessDefinitionConverter converter,
          @Autowired(required = false) List<ProcessRuntimeEventListener<ProcessDeployedEvent>> listeners,
          ApplicationEventPublisher eventPublisher
      ) {
          return new ProcessDeployedEventProducer(
              repositoryService,
              converter,
              Optional.ofNullable(listeners).orElse(emptyList()),
              eventPublisher
          );
      }
  
      @Bean
      @ConditionalOnMissingBean
      public ProcessCandidateStartersEventProducer processCandidateStartersEventProducer(
          RepositoryService repositoryService,
          @Autowired(required = false) List<
              ProcessRuntimeEventListener<ProcessCandidateStarterUserAddedEvent>
          > candidateStarterUserListeners,
          @Autowired(required = false) List<
              ProcessRuntimeEventListener<ProcessCandidateStarterGroupAddedEvent>
          > candidateStarterGroupListeners,
          ApplicationEventPublisher eventPublisher
      ) {
          return new ProcessCandidateStartersEventProducer(
              repositoryService,
              candidateStarterUserListeners,
              candidateStarterGroupListeners,
              eventPublisher
          );
      }
  
      @Bean
      @ConditionalOnMissingBean
      public StartMessageDeployedEventProducer startMessageDeployedEventProducer(
          RepositoryService repositoryService,
          ManagementService managementService,
          StartMessageSubscriptionConverter subscriptionConverter,
          APIProcessDefinitionConverter converter,
          List<ProcessRuntimeEventListener<StartMessageDeployedEvent>> listeners,
          ApplicationEventPublisher eventPublisher
      ) {
          return new StartMessageDeployedEventProducer(
              repositoryService,
              managementService,
              subscriptionConverter,
              converter,
              listeners,
              eventPublisher
          );
      }
  
      @Bean(name = BEHAVIOR_FACTORY_MAPPING_CONFIGURER)
      @ConditionalOnMissingBean(name = BEHAVIOR_FACTORY_MAPPING_CONFIGURER)
      public DefaultActivityBehaviorFactoryMappingConfigurer defaultActivityBehaviorFactoryMappingConfigurer(
          ExtensionsVariablesMappingProvider variablesMappingProvider,
          ProcessVariablesInitiator processVariablesInitiator,
          EventSubscriptionPayloadMappingProvider eventSubscriptionPayloadMappingProvider,
          VariablesPropagator variablesPropagator
      ) {
          return new DefaultActivityBehaviorFactoryMappingConfigurer(
              variablesMappingProvider,
              processVariablesInitiator,
              eventSubscriptionPayloadMappingProvider,
              variablesPropagator
          );
      }
  
      @Bean
      @Order(Ordered.HIGHEST_PRECEDENCE)
      public ProcessEngineConfigurationConfigurer asyncExecutorPropertiesConfigurer(AsyncExecutorProperties properties) {
          return configuration -> {
              configuration.setAsyncExecutorMessageQueueMode(properties.isMessageQueueMode());
              configuration.setAsyncExecutorCorePoolSize(properties.getCorePoolSize());
              configuration.setAsyncExecutorAsyncJobLockTimeInMillis(properties.getAsyncJobLockTimeInMillis());
              configuration.setAsyncExecutorNumberOfRetries(properties.getNumberOfRetries());
  
              configuration.setAsyncExecutorDefaultAsyncJobAcquireWaitTime(
                  properties.getDefaultAsyncJobAcquireWaitTimeInMillis()
              );
              configuration.setAsyncExecutorDefaultTimerJobAcquireWaitTime(
                  properties.getDefaultTimerJobAcquireWaitTimeInMillis()
              );
              configuration.setAsyncExecutorDefaultQueueSizeFullWaitTime(properties.getDefaultQueueSizeFullWaitTime());
  
              configuration.setAsyncExecutorMaxAsyncJobsDuePerAcquisition(properties.getMaxAsyncJobsDuePerAcquisition());
              configuration.setAsyncExecutorMaxTimerJobsPerAcquisition(properties.getMaxTimerJobsPerAcquisition());
              configuration.setAsyncExecutorMaxPoolSize(properties.getMaxPoolSize());
  
              configuration.setAsyncExecutorResetExpiredJobsInterval(properties.getResetExpiredJobsInterval());
              configuration.setAsyncExecutorResetExpiredJobsPageSize(properties.getResetExpiredJobsPageSize());
  
              configuration.setAsyncExecutorSecondsToWaitOnShutdown(properties.getSecondsToWaitOnShutdown());
              configuration.setAsyncExecutorThreadKeepAliveTime(properties.getKeepAliveTime());
              configuration.setAsyncExecutorTimerLockTimeInMillis(properties.getTimerLockTimeInMillis());
              configuration.setAsyncExecutorThreadPoolQueueSize(properties.getQueueSize());
  
              configuration.setAsyncFailedJobWaitTime(properties.getRetryWaitTimeInMillis());
          };
      }
  
      @Bean
      @ConditionalOnMissingBean
      public ApplicationDeployedEventProducer applicationDeployedEventProducer(
          RepositoryService repositoryService,
          APIDeploymentConverter converter,
          @Autowired(required = false) List<ProcessRuntimeEventListener<ApplicationDeployedEvent>> listeners,
          ApplicationEventPublisher eventPublisher
      ) {
          return new ApplicationDeployedEventProducer(
              repositoryService,
              converter,
              Optional.ofNullable(listeners).orElse(emptyList()),
              eventPublisher
          );
      }
  
      @Bean
      @ConditionalOnMissingBean
      public CandidateStartersDeploymentConfigurer candidateStartersDeploymentConfigurer() {
          return new CandidateStartersDeploymentConfigurer();
      }
  
      @Bean
      @ConditionalOnMissingBean
      @ConditionalOnProperty("spring.activiti.process-definition-cache-name")
      public DeploymentCache<ProcessDefinitionCacheEntry> springProcessDefinitionCache(
          ActivitiProperties properties,
          CacheManager cacheManager
      ) {
          var delegate = cacheManager.getCache(properties.getProcessDefinitionCacheName());
  
          return new SpringProcessDefinitionCache(Objects.requireNonNull(delegate));
      }
  }
  