diff --git a/.idea/encodings.xml b/.idea/encodings.xml index b4b2816..ea77922 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -7,6 +7,10 @@ + + + + diff --git a/ordo-spring-boot-autoconfigure/pom.xml b/ordo-spring-boot-autoconfigure/pom.xml new file mode 100644 index 0000000..d819c7c --- /dev/null +++ b/ordo-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + com.jetlumen + ordo + 1.0-SNAPSHOT + + + ordo-spring-boot-autoconfigure + + + 21 + 21 + UTF-8 + 3.3.5 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + com.jetlumen + ordo-api + ${project.version} + + + com.jetlumen + ordo-core + ${project.version} + + + com.jetlumen + ordo-storage-jdbc + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.flywaydb + flyway-core + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + test + + + + \ No newline at end of file diff --git a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java new file mode 100644 index 0000000..c14b5cb --- /dev/null +++ b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java @@ -0,0 +1,98 @@ +package com.jetlumen.ordo.spring; + +import com.jetlumen.ordo.api.AssigneeResolver; +import com.jetlumen.ordo.api.OrdoEngine; +import com.jetlumen.ordo.api.TransactionExecutor; +import com.jetlumen.ordo.api.repository.ApprovalTaskRepository; +import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository; +import com.jetlumen.ordo.api.repository.ProcessInstanceRepository; +import com.jetlumen.ordo.core.DefaultOrdoEngine; +import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository; +import com.jetlumen.ordo.storage.jdbc.JdbcConnectionProvider; +import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository; +import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository; +import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization; +import org.springframework.context.annotation.Bean; + +import javax.sql.DataSource; +import java.time.Clock; + +/** + * Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is + * present. Any bean already defined by the application (e.g. a custom + * {@link AssigneeResolver} or repository implementation) takes precedence over the + * defaults created here. + */ +@AutoConfiguration +@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnClass(DataSource.class) +@ConditionalOnBean(DataSource.class) +@AutoConfigureAfter({DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class}) +@EnableConfigurationProperties(OrdoProperties.class) +public class OrdoJdbcAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public Clock ordoClock() { + return Clock.systemUTC(); + } + + @Bean + @ConditionalOnMissingBean + public AssigneeResolver ordoAssigneeResolver() { + return AssigneeResolver.direct(); + } + + @Bean + @ConditionalOnMissingBean + @DependsOnDatabaseInitialization + public JdbcConnectionProvider ordoJdbcConnectionProvider(DataSource dataSource) { + return new JdbcConnectionProvider(dataSource); + } + + @Bean + @ConditionalOnMissingBean + public TransactionExecutor ordoTransactionExecutor(JdbcConnectionProvider connectionProvider) { + return new JdbcTransactionExecutor(connectionProvider); + } + + @Bean + @ConditionalOnMissingBean + public ProcessDefinitionRepository ordoProcessDefinitionRepository(JdbcConnectionProvider connectionProvider) { + return new JdbcProcessDefinitionRepository(connectionProvider); + } + + @Bean + @ConditionalOnMissingBean + public ProcessInstanceRepository ordoProcessInstanceRepository(JdbcConnectionProvider connectionProvider) { + return new JdbcProcessInstanceRepository(connectionProvider); + } + + @Bean + @ConditionalOnMissingBean + public ApprovalTaskRepository ordoApprovalTaskRepository(JdbcConnectionProvider connectionProvider) { + return new JdbcApprovalTaskRepository(connectionProvider); + } + + @Bean + @ConditionalOnMissingBean + public OrdoEngine ordoEngine(Clock ordoClock, + AssigneeResolver ordoAssigneeResolver, + TransactionExecutor ordoTransactionExecutor, + ProcessDefinitionRepository ordoProcessDefinitionRepository, + ProcessInstanceRepository ordoProcessInstanceRepository, + ApprovalTaskRepository ordoApprovalTaskRepository) { + return new DefaultOrdoEngine(ordoClock, ordoAssigneeResolver, ordoTransactionExecutor, + ordoProcessDefinitionRepository, ordoProcessInstanceRepository, ordoApprovalTaskRepository); + } +} diff --git a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java new file mode 100644 index 0000000..b7d353d --- /dev/null +++ b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java @@ -0,0 +1,19 @@ +package com.jetlumen.ordo.spring; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Configuration properties for the Ordo Spring Boot autoconfiguration. */ +@ConfigurationProperties(prefix = "ordo") +public class OrdoProperties { + + /** Whether the Ordo JDBC autoconfiguration should be applied. */ + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f5adb01 --- /dev/null +++ b/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration diff --git a/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java new file mode 100644 index 0000000..445c000 --- /dev/null +++ b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java @@ -0,0 +1,86 @@ +package com.jetlumen.ordo.spring; + +import com.jetlumen.ordo.api.ApprovalStep; +import com.jetlumen.ordo.api.ApprovalTask; +import com.jetlumen.ordo.api.AssigneeResolver; +import com.jetlumen.ordo.api.OrdoEngine; +import com.jetlumen.ordo.api.ProcessDefinition; +import com.jetlumen.ordo.api.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class OrdoJdbcAutoConfigurationTest { + + private static final ProcessDefinition LEAVE_REQUEST = new ProcessDefinition("leave-request", "Leave request", + List.of(new ApprovalStep("manager", "Manager approval", "maria"))); + + private final ApplicationContextRunner withDataSourceRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class)) + .withPropertyValues( + "spring.datasource.url=jdbc:h2:mem:ordo_" + UUID.randomUUID() + + ";MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", + "spring.datasource.username=sa"); + + private final ApplicationContextRunner withoutDataSourceRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OrdoJdbcAutoConfiguration.class)); + + @Test + void assemblesJdbcBackedEngineWhenDataSourceIsPresent() { + withDataSourceRunner.run(context -> { + assertThat(context).hasSingleBean(OrdoEngine.class); + OrdoEngine engine = context.getBean(OrdoEngine.class); + + engine.register(LEAVE_REQUEST); + ProcessInstance instance = engine.start("leave-request", "alice"); + List pending = engine.findPendingTasksByAssignee("maria"); + assertThat(pending).hasSize(1); + + ApprovalTask approved = engine.approve(pending.get(0).id(), "maria", "looks good"); + assertThat(approved.status().name()).isEqualTo("APPROVED"); + assertThat(engine.findInstance(instance.id())).isPresent(); + }); + } + + @Test + void backsOffWhenNoDataSourceBeanIsPresent() { + withoutDataSourceRunner.run(context -> + assertThat(context).doesNotHaveBean(OrdoEngine.class)); + } + + @Test + void backsOffWhenDisabledViaProperty() { + withDataSourceRunner.withPropertyValues("ordo.enabled=false") + .run(context -> + assertThat(context).doesNotHaveBean(OrdoEngine.class)); + } + + @Test + void honoursUserDefinedAssigneeResolver() { + withDataSourceRunner.withUserConfiguration(CustomAssigneeResolverConfig.class) + .run(context -> { + assertThat(context).hasSingleBean(OrdoEngine.class); + assertThat(context.getBean(AssigneeResolver.class)).isNotNull(); + assertThat(context.getBean(AssigneeResolver.class).resolve(null, null)) + .isEqualTo("custom-assignee"); + }); + } + + @Configuration + static class CustomAssigneeResolverConfig { + @Bean + AssigneeResolver ordoAssigneeResolver() { + return (step, ctx) -> "custom-assignee"; + } + } +} diff --git a/ordo-spring-boot-starter/pom.xml b/ordo-spring-boot-starter/pom.xml new file mode 100644 index 0000000..ef9e4d8 --- /dev/null +++ b/ordo-spring-boot-starter/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.jetlumen + ordo + 1.0-SNAPSHOT + + + ordo-spring-boot-starter + + + 21 + 21 + UTF-8 + 3.3.5 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + com.jetlumen + ordo-spring-boot-autoconfigure + ${project.version} + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.flywaydb + flyway-core + + + + org.flywaydb + flyway-database-postgresql + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 87dbdba..67ca131 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,8 @@ ordo-api ordo-example ordo-storage-jdbc + ordo-spring-boot-starter + ordo-spring-boot-autoconfigure