Compare commits
6
Commits
e4e2c6ed9b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e773badb23 | ||
|
|
056ab7a806 | ||
|
|
02557a9b86 | ||
|
|
8d08a2840b | ||
|
|
f0ddaf7694 | ||
|
|
d743939964 |
@@ -1,6 +1,6 @@
|
||||
# Ordo
|
||||
|
||||
轻量审批流程引擎。宿主通过 `OrdoEngine` 注册流程定义、发起实例、审批/驳回/转派/撤回/取消,并查询任务、实例与审计历史。引擎不绑定业务表单、用户体系或设计器 UI,当前也不自带 REST;业务数据放在 `ProcessContext` 里。
|
||||
轻量审批流程引擎。宿主通过 `OrdoEngine` 注册流程定义、发起实例、审批/驳回/转派/撤回/取消,并查询任务、实例与审计历史。引擎不绑定业务表单、用户体系或设计器 UI。可选 REST(`ordo.rest.enabled`)。业务数据放在 `ProcessContext` 里。
|
||||
|
||||
要求 **Java 17+**。当前版本 `0.0.1-SNAPSHOT`。
|
||||
|
||||
@@ -14,23 +14,23 @@
|
||||
| `ordo-spring-boot-starter` | Spring Boot 自动装配(JDBC + Flyway) |
|
||||
| `ordo-example` | 内存引擎示例 |
|
||||
|
||||
存储实现通过 dialect 层支持 **PostgreSQL** 与 **MySQL**(测试可用 H2 PostgreSQL 兼容模式)。Starter **不携带** JDBC 驱动,宿主自行加入 `postgresql`、`mysql-connector-j` 或 `h2`。Spring Boot 4 还需额外引入 `spring-boot-starter-flyway`,否则迁移不会跑。
|
||||
存储实现通过 dialect 层支持 **PostgreSQL** 与 **MySQL**(测试可用 H2 PostgreSQL 兼容模式)。Starter **不携带** JDBC 驱动,宿主自行加入 `postgresql`、`mysql-connector-j` 或 `h2`。表结构由 Ordo 自己的 Flyway 迁移(`ordo_schema_history`),与宿主 `spring.flyway` 无关。
|
||||
|
||||
## 能力
|
||||
|
||||
- 线性或多步图:`StepTransition` + 内置谓词 / 宿主 `RoutingCondition`
|
||||
- 会签/或签:`ApprovalPolicy.ALL` / `ANY`(多候选人)
|
||||
- 结构化并行:`kind: PARALLEL` 块(一层、join ALL)
|
||||
- ACTION 步骤:事务提交后调用宿主 `ActionHandler`
|
||||
- ACTION 步骤:事务提交后调用宿主 `ActionHandler`(可经 `NamedAction` 分发)
|
||||
- 发起人撤回:`WITHDRAWN`,待办任务 `SKIPPED`
|
||||
- 管理员/系统取消:`cancel` → `CANCELLED`,待办任务 `SKIPPED`(引擎不鉴权角色)
|
||||
- 任务转派:当前办理人 `reassign`,审计 `TASK_REASSIGNED`
|
||||
- 到期升级:步骤 `due` + `processDue`,审计 `TASK_ESCALATED`
|
||||
- 分页查询:任务 / 实例 / 流程定义
|
||||
- 审计时间线:`ProcessEvent` + `queryHistory`
|
||||
- 扩展点:`AssigneeResolver`、`RoutingCondition`、`ActionHandler`、`OrdoEventListener`
|
||||
- 扩展点:`AssigneeResolver`、`RoutingCondition`、`ActionHandler`、`NamedCondition`、`NamedAction`、`OrdoEventListener`
|
||||
|
||||
开发计划:可选 REST + 目录 SPI。设计器为独立产品(不进本仓库),待 REST 与目录之后。多租户 **暂不在计划中**。见 [docs/roadmap.md](docs/roadmap.md)。
|
||||
开发计划:独立设计器(不进本仓库)。多租户 **暂不在计划中**。见 [docs/roadmap.md](docs/roadmap.md)。
|
||||
|
||||
详细用法(定义 JSON、扩展点、异常、查询、ACTION/审计语义)见 **[docs/usage.md](docs/usage.md)**。对外行为变更时同步更新该文档。
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
```
|
||||
|
||||
```java
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.core.InMemoryOrdoEngine;
|
||||
|
||||
OrdoEngine ordo = new InMemoryOrdoEngine();
|
||||
ordo.publish(ProcessDefinition.linear("leave-request", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
@@ -83,8 +89,7 @@ ordo.approve(manager.id(), "maria", "ok");
|
||||
|
||||
- `kind` 默认 `APPROVAL`;ACTION 用 `"action"` 作为 handler 查找键;`PARALLEL` 用 `branches`(结构化并行块,见 [docs/usage.md](docs/usage.md))。
|
||||
- 带 `when` 的边先按 `priority` 匹配,都未命中再走无条件边;`to: null` 表示结束。
|
||||
- 代码侧可用 `ProcessDefinitionParser.fromJson(...)`。
|
||||
- `replace` 会整体替换同 id 定义;存在 `RUNNING` 实例时拒绝替换。
|
||||
- 代码侧可用 `ProcessDefinitionParser.fromJson(...)`(`com.jetlumen.ordo.api.definition`)。
|
||||
|
||||
## Spring Boot
|
||||
|
||||
@@ -96,7 +101,7 @@ ordo.approve(manager.id(), "maria", "ok");
|
||||
</dependency>
|
||||
```
|
||||
|
||||
需要 `DataSource`。存在 `DataSource` 且 `ordo.enabled` 不为 `false` 时装配 JDBC 引擎。
|
||||
需要 `DataSource`。存在 `DataSource` 且 `ordo.enabled` 不为 `false` 时装配 JDBC 引擎。REST 另需宿主的 Web starter,且 `ordo.rest.enabled=true`。
|
||||
|
||||
```yaml
|
||||
ordo:
|
||||
@@ -105,6 +110,12 @@ ordo:
|
||||
dialect: # 可选 postgresql / mysql;空则按 DataSource 探测
|
||||
definitions:
|
||||
location: classpath*:ordo/*.json # 默认值;启动时 publish 加载
|
||||
due:
|
||||
poll-ms: 0 # >0 启用调度;值为最长空闲,按下次 dueAt 唤醒,满批续拉
|
||||
batch-size: 100
|
||||
rest:
|
||||
enabled: false
|
||||
base-path: /ordo
|
||||
```
|
||||
|
||||
宿主提供 Bean 即可覆盖默认值:
|
||||
@@ -112,8 +123,10 @@ ordo:
|
||||
| Bean | 默认 |
|
||||
|---|---|
|
||||
| `AssigneeResolver` | 候选人即办理人 |
|
||||
| `RoutingCondition` | 始终匹配(`ref` 边;宿主可按 key+args 分发) |
|
||||
| `ActionHandler` | 空操作 |
|
||||
| `NamedCondition` / `NamedAction` | 可多个;无对应门面时按 key 分发 |
|
||||
| `RoutingCondition` | 无具名 condition 时始终匹配;有则未知 `ref` 为 false |
|
||||
| `ActionHandler` | 无具名 action 时空操作;有则未知 key 抛错 |
|
||||
| `OrdoCatalog` | REST 打开时从 Named* 投影;`assignees` 空 |
|
||||
| `OrdoEventListener` | 可注册多个,提交后按顺序调用 |
|
||||
|
||||
## 运行时约定
|
||||
|
||||
@@ -471,6 +471,7 @@ components:
|
||||
label:
|
||||
type: string
|
||||
ProcessDefinitionDocument:
|
||||
description: Closed document shape is docs/process-definition.schema.json. Graph rules are enforced by ProcessDefinitionParser.
|
||||
type: object
|
||||
required: [id, name, steps, transitions]
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://jetlumen.com/ordo/schema/process-definition.json",
|
||||
"title": "Ordo process definition document",
|
||||
"$comment": "Document shape for ProcessDefinitionParser.fromJson. Graph connectivity, unique ids, PARALLEL membership, due.goto branch rules, and predicate depth/leaf limits are enforced only by the parser.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "steps", "transitions"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"name": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"$comment": "Ignored by fromJson; written by toJson after publish."
|
||||
},
|
||||
"startStep": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/topStep" }
|
||||
},
|
||||
"transitions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/transition" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"nonEmptyString": {
|
||||
"type": "string",
|
||||
"pattern": ".*\\S.*"
|
||||
},
|
||||
"literal": {
|
||||
"type": ["string", "number", "boolean"]
|
||||
},
|
||||
"topStep": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/approvalStep" },
|
||||
{ "$ref": "#/$defs/actionStep" },
|
||||
{ "$ref": "#/$defs/parallelStep" }
|
||||
]
|
||||
},
|
||||
"leafStep": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/approvalStep" },
|
||||
{ "$ref": "#/$defs/actionStep" }
|
||||
]
|
||||
},
|
||||
"approvalStep": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "candidates"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"name": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"kind": { "const": "APPROVAL" },
|
||||
"candidates": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/nonEmptyString" }
|
||||
},
|
||||
"policy": { "type": "string", "enum": ["ANY", "ALL"] },
|
||||
"due": { "$ref": "#/$defs/due" }
|
||||
}
|
||||
},
|
||||
"actionStep": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "kind", "action"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"name": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"kind": { "const": "ACTION" },
|
||||
"action": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"policy": { "type": "string", "enum": ["ANY", "ALL"] }
|
||||
}
|
||||
},
|
||||
"parallelStep": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "kind", "branches"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"name": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"kind": { "const": "PARALLEL" },
|
||||
"policy": { "type": "string", "enum": ["ANY", "ALL"] },
|
||||
"branches": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": { "$ref": "#/$defs/branch" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"branch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "steps", "transitions"],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/leafStep" }
|
||||
},
|
||||
"transitions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/transition" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"due": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["after", "then", "to"],
|
||||
"properties": {
|
||||
"after": { "$ref": "#/$defs/isoDuration" },
|
||||
"then": { "const": "reassign" },
|
||||
"to": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["after", "then"],
|
||||
"properties": {
|
||||
"after": { "$ref": "#/$defs/isoDuration" },
|
||||
"then": { "const": "notify" },
|
||||
"action": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["after", "then", "to"],
|
||||
"properties": {
|
||||
"after": { "$ref": "#/$defs/isoDuration" },
|
||||
"then": { "const": "goto" },
|
||||
"to": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"isoDuration": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"$comment": "java.time.Duration.parse; must be positive after parse."
|
||||
},
|
||||
"transition": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["from"],
|
||||
"properties": {
|
||||
"from": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"to": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1
|
||||
},
|
||||
"when": { "$ref": "#/$defs/when" },
|
||||
"priority": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"when": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/refWhen" },
|
||||
{ "$ref": "#/$defs/predicate" }
|
||||
]
|
||||
},
|
||||
"refWhen": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ref"],
|
||||
"properties": {
|
||||
"ref": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"args": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"predicate": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/compareEq" },
|
||||
{ "$ref": "#/$defs/compareNe" },
|
||||
{ "$ref": "#/$defs/compareGt" },
|
||||
{ "$ref": "#/$defs/compareGte" },
|
||||
{ "$ref": "#/$defs/compareLt" },
|
||||
{ "$ref": "#/$defs/compareLte" },
|
||||
{ "$ref": "#/$defs/inPredicate" },
|
||||
{ "$ref": "#/$defs/andPredicate" },
|
||||
{ "$ref": "#/$defs/orPredicate" },
|
||||
{ "$ref": "#/$defs/notPredicate" }
|
||||
]
|
||||
},
|
||||
"comparePair": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/nonEmptyString" },
|
||||
{ "$ref": "#/$defs/literal" }
|
||||
]
|
||||
},
|
||||
"compareEq": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["eq"],
|
||||
"properties": { "eq": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"compareNe": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ne"],
|
||||
"properties": { "ne": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"compareGt": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["gt"],
|
||||
"properties": { "gt": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"compareGte": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["gte"],
|
||||
"properties": { "gte": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"compareLt": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["lt"],
|
||||
"properties": { "lt": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"compareLte": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["lte"],
|
||||
"properties": { "lte": { "$ref": "#/$defs/comparePair" } }
|
||||
},
|
||||
"inPredicate": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["in"],
|
||||
"properties": {
|
||||
"in": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/nonEmptyString" },
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/literal" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"andPredicate": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["and"],
|
||||
"properties": {
|
||||
"and": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/predicate" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"orPredicate": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["or"],
|
||||
"properties": {
|
||||
"or": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/predicate" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"notPredicate": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["not"],
|
||||
"properties": {
|
||||
"not": { "$ref": "#/$defs/predicate" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
- 到期升级 `processDue` / `TASK_ESCALATED`
|
||||
- 管理员/系统取消 `cancel` / `CANCELLED`
|
||||
- 可选 REST(autoconfigure 条件装配)+ 目录 SPI `OrdoCatalog`
|
||||
- `NamedAction` / `NamedCondition` 注册与官方 key 分发;默认 Catalog 从具名 Bean 投影
|
||||
- API 分包:`api.definition` / `runtime` / `spi` / `util`
|
||||
|
||||
## 开发计划(确定要做)
|
||||
|
||||
|
||||
+47
-19
@@ -22,7 +22,19 @@ Ordo 是嵌入宿主进程的审批引擎,入口是 `OrdoEngine`。
|
||||
| `ordo-spring-boot-starter` | Spring Boot 自动装配 |
|
||||
| `ordo-example` | `LeaveRequestExample` 内存演示 |
|
||||
|
||||
Starter **不携带** JDBC 驱动。生产按库添加 `org.postgresql:postgresql` 或 `com.mysql:mysql-connector-j`。Spring Boot 4 还需 `spring-boot-starter-flyway`,否则 Flyway 迁移不会执行。
|
||||
Java 包(模块未变):
|
||||
|
||||
| 包 | 内容 |
|
||||
|---|---|
|
||||
| `com.jetlumen.ordo.api` | `OrdoEngine`、`TransactionExecutor` |
|
||||
| `com.jetlumen.ordo.api.definition` | 图:定义、步骤、边、`when`、PARALLEL |
|
||||
| `com.jetlumen.ordo.api.runtime` | 实例、任务、事件、ACTION 执行、token、`ProcessRuntime` |
|
||||
| `com.jetlumen.ordo.api.spi` | `ActionHandler`、`NamedAction`、`RoutingCondition`、`NamedCondition`、`AssigneeResolver`、`OrdoCatalog`、`OrdoEventListener` |
|
||||
| `com.jetlumen.ordo.api.util` | `Texts`、`Jsons` |
|
||||
| `com.jetlumen.ordo.api.exception` / `query` / `repository` | 异常、分页查询、存储端口 |
|
||||
| `com.jetlumen.ordo.core.spi` | `DispatchingActionHandler`、`DispatchingRoutingCondition`、`RegistryOrdoCatalog` |
|
||||
|
||||
Starter **不携带** JDBC 驱动。生产按库添加 `org.postgresql:postgresql` 或 `com.mysql:mysql-connector-j`。Ordo 用独立 Flyway 建表,不依赖宿主 `spring.flyway`。
|
||||
|
||||
先 `mvn install` 本仓库,宿主再依赖 `0.0.1-SNAPSHOT`。
|
||||
|
||||
@@ -60,10 +72,13 @@ ordo:
|
||||
enabled: true
|
||||
jdbc:
|
||||
dialect: # 可选 postgresql / mysql;空则按 DataSource 探测
|
||||
migrate: true
|
||||
history-table: ordo_schema_history
|
||||
definitions:
|
||||
location: classpath*:ordo/*.json # 启动时对每个 JSON 调用 publish
|
||||
due:
|
||||
poll-ms: 0 # >0 时轮询 processDue;默认不调度
|
||||
poll-ms: 0 # >0 启用调度;值为最长空闲,按下次 dueAt 唤醒,满批续拉
|
||||
batch-size: 100
|
||||
rest:
|
||||
enabled: false
|
||||
base-path: /ordo
|
||||
@@ -76,12 +91,16 @@ ordo:
|
||||
| Bean | 默认 |
|
||||
|---|---|
|
||||
| `AssigneeResolver` | 候选人字符串即办理人 |
|
||||
| `RoutingCondition` | `ref` 边默认恒 true;无 `when` 的边不经过该 Bean |
|
||||
| `ActionHandler` | 空操作 |
|
||||
| `NamedCondition` | 可有多个;无 `RoutingCondition` 门面且列表非空时装配 `DispatchingRoutingCondition` |
|
||||
| `NamedAction` | 可有多个;无 `ActionHandler` 门面且列表非空时装配 `DispatchingActionHandler` |
|
||||
| `RoutingCondition` | 无 `NamedCondition` 时 `ref` 边恒 true;有则按 key 分发,未知 `ref` 为 false |
|
||||
| `ActionHandler` | 无 `NamedAction` 时空操作;有则按 key 分发,未知 key 抛 `IllegalArgumentException` |
|
||||
| `OrdoEventListener` | 可有多个,提交后按 Spring 顺序调用 |
|
||||
| `OrdoCatalog` | 空列表(仅 REST 打开时装配) |
|
||||
| `OrdoCatalog` | REST 打开时:从 `NamedCondition` / `NamedAction` 投影;皆空则为空列表。`assignees` 仍空 |
|
||||
|
||||
未提供自定义 Bean 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。
|
||||
未提供 `NamedAction` 且未覆盖 `ActionHandler` 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。不要同时提供门面 Bean 与对应 `Named*`(门面优先,具名 Bean 不参与运行时)。
|
||||
|
||||
内存引擎把 `com.jetlumen.ordo.core.spi.DispatchingRoutingCondition.of(...)` / `DispatchingActionHandler.of(...)` 传入 `InMemoryOrdoEngine` 即可。
|
||||
|
||||
### 2.3 可选 REST
|
||||
|
||||
@@ -157,7 +176,7 @@ new ProcessDefinition("leave-request-routed", "Leave request",
|
||||
|
||||
### 3.2 JSON
|
||||
|
||||
`ProcessDefinitionParser.fromJson(String|InputStream)` / `toJson(ProcessDefinition)`。Spring 默认扫 `classpath*:ordo/*.json`。`toJson` 写出 `version` 与 `startStep`(当前步骤列表首位);`fromJson` 仍忽略 JSON 里的 `version`。
|
||||
`ProcessDefinitionParser.fromJson(String|InputStream)` / `toJson(ProcessDefinition)`。Spring 默认扫 `classpath*:ordo/*.json`。`toJson` 写出 `version` 与 `startStep`(当前步骤列表首位);`fromJson` 仍忽略 JSON 里的 `version`。文档外形:[process-definition.schema.json](process-definition.schema.json);图连通、PARALLEL 约束、谓词深度/叶子上限仍以解析器为准。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -246,7 +265,8 @@ ordo.cancel(instance.id(), "admin", "政策变更");
|
||||
|
||||
- `approve` / `reject` / `reassign`:`actor` 必须等于该任务当前 `assignee`,否则 `UnauthorizedTaskOperationException`。
|
||||
- `reassign`:仅 `PENDING` 任务;同一任务 id,办理人改为 `newAssignee`,不推进步骤。`newAssignee` 不可空白、不可等于当前 `assignee`,且同一步不能已有该人的 `PENDING` 任务,否则 `IllegalArgumentException`。不经过 `AssigneeResolver`。人工转派不改 `dueAt`。
|
||||
- `processDue(limit)`:认领 `dueAt <= now` 的 PENDING 任务(`limit > 0`),按步上 `due.then` 执行:`reassign` 换办理人(`to` 走 `AssigneeResolver`)、`notify` 可选 `ActionHandler`、`goto` 跳过当前步 PENDING 并进入 `to` 步骤(`to` 必须是步骤 id)。每种策略对一张任务最多成功一次(清空 `dueAt`)。引擎无后台线程;Spring 下 `ordo.due.poll-ms > 0` 才轮询。
|
||||
- `processDue(limit)`:认领 `dueAt <= now` 的 PENDING 任务(`limit > 0`),按步上 `due.then` 执行:`reassign` 换办理人(`to` 走 `AssigneeResolver`)、`notify` 可选 `ActionHandler`、`goto` 跳过当前步 PENDING 并进入 `to` 步骤(`to` 必须是步骤 id)。每种策略对一张任务最多成功一次(清空 `dueAt`)。引擎无后台线程;Spring 下 `ordo.due.poll-ms > 0` 才调度:按下次 `dueAt` 唤醒,满批续拉,`poll-ms` 为最长空闲。
|
||||
- `nextDueAt()`:PENDING 且仍有 `dueAt` 的最早到期时刻;没有则 empty。
|
||||
- 任务非 `PENDING`:`TaskAlreadyCompletedException`。
|
||||
- `withdraw`:仅 `initiator`,否则 `UnauthorizedInstanceOperationException`;实例非 `RUNNING`:`InstanceAlreadyCompletedException`。
|
||||
- `cancel`:`actor` 非空即可,**不校验**是否发起人;实例须为 `RUNNING`,否则 `InstanceAlreadyCompletedException`。谁能调用由宿主决定。
|
||||
@@ -294,9 +314,9 @@ v1:至少 2 条分支;禁止套娃 PARALLEL;join 固定 ALL;任一分支
|
||||
|
||||
无条件边是默认分支;`priority` 只在同类边之间比较(多条条件边之间,或多条无条件边之间)。
|
||||
|
||||
`RoutingCondition.matches` 看到 `args` 与 `ProcessRuntime`(含业务 `context`、发起人、定义与当前步)。看不到任务意见。上下文在 `start` 时写入,运行中引擎不会改 context。未知 `ref` 由宿主返回 `false`,该边不匹配。内置谓词仍只读 `ProcessContext` 变量。
|
||||
`RoutingCondition.matches` 看到 `args` 与 `ProcessRuntime`(含业务 `context`、发起人、定义与当前步)。看不到任务意见。上下文在 `start` 时写入,运行中引擎不会改 context。内置谓词仍只读 `ProcessContext` 变量。
|
||||
|
||||
引擎只注入**一个** `RoutingCondition`。Spring 下多个该类型 Bean 会冲突。宿主用一个门面按 `ref` 分发;不要指望引擎按定义拆 bean。
|
||||
引擎仍只注入**一个** `RoutingCondition`。Spring 下注册多个 `NamedCondition` Bean(`key()` 对应 `when.ref`);无门面且列表非空时装配官方分发器。未知 `ref` 为 false,该边不匹配。重复 `key` 启动失败。也可继续提供单个 `RoutingCondition` 门面自行分发(不要与 `NamedCondition` 混用)。无 `NamedCondition` 时默认仍是恒 true。
|
||||
|
||||
## 7. ACTION 步骤
|
||||
|
||||
@@ -307,22 +327,29 @@ v1:至少 2 条分支;禁止套娃 PARALLEL;join 固定 ALL;任一分支
|
||||
3. 事务提交后调用 `ActionHandler.execute(actionKey, runtime)`。
|
||||
4. 成功 → `SUCCESS` + 事件 `ACTION_SUCCEEDED`;失败 → `FAILED`(`errorMessage`)+ `ACTION_FAILED` + 日志 WARNING。
|
||||
|
||||
**失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。
|
||||
**失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。分发器遇到未知 `actionKey` 会抛 `IllegalArgumentException`,记为该次 ACTION `FAILED`。
|
||||
|
||||
```java
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MailActions implements ActionHandler {
|
||||
public class LeaveApprovedMail implements NamedAction {
|
||||
@Override
|
||||
public void execute(String actionKey, ProcessRuntime runtime) {
|
||||
switch (actionKey) {
|
||||
case "leave-submitted-mail" -> { /* ... */ }
|
||||
case "leave-approved-mail" -> { /* ... */ }
|
||||
default -> throw new IllegalStateException("unknown action: " + actionKey);
|
||||
}
|
||||
public String key() {
|
||||
return "leave-approved-mail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
仍可提供单个 `ActionHandler` 按 key 自行分发;不要与 `NamedAction` 混用。
|
||||
|
||||
## 8. 审计与监听
|
||||
|
||||
`queryHistory(instanceId, page)`:该实例事件,**发生时间升序**。
|
||||
@@ -369,6 +396,7 @@ ACTION 成功事件发生在提交之后,因此排在同轮事务内写入的
|
||||
| `approve` / `reject` | 办理当前 PENDING 任务 |
|
||||
| `reassign` | 当前办理人把 PENDING 任务转给他人 |
|
||||
| `processDue` | 认领并处理已到期 PENDING 任务 |
|
||||
| `nextDueAt` | 下一笔 PENDING 任务的 `dueAt` |
|
||||
| `withdraw` | 发起人撤回 |
|
||||
| `cancel` | 管理员/系统取消(引擎不鉴权角色) |
|
||||
| `find*` | 按 id / 待办索引读取 |
|
||||
@@ -394,7 +422,7 @@ ACTION 成功事件发生在提交之后,因此排在同轮事务内写入的
|
||||
|
||||
## 12. 存储
|
||||
|
||||
Flyway 脚本按方言分目录:`db/postgresql/migration`、`db/mysql/migration`。未设置 `spring.flyway.locations` 时,starter 按探测到的方言指向对应目录。已有 `flyway_schema_history` 的开发库按版本追加迁移(PARALLEL 为 `V3__parallel_blocks.sql`)。表包括流程头 `ordo_process`、按 `(id, version)` 存储的定义/步骤(含 `parent_step_id` / `branch_id`)/候选人/转移、实例(含 `definition_version`)、任务、并行令牌 `ordo_instance_token`、`ordo_process_event`、`ordo_action_execution`。
|
||||
Flyway 脚本按方言分目录:`db/postgresql/migration`、`db/mysql/migration`。Starter 在启动时用独立 Flyway 执行这些脚本,历史表默认 `ordo_schema_history`,不修改 `spring.flyway.locations`。若宿主也启用了 Spring Boot Flyway,Ordo 会在宿主 `flywayInitializer` 之后再 migrate,避免非空 schema 导致宿主失败。已有 `ordo_*` 表但尚无该历史表时,会 baseline 到当前脚本最高版本后再 migrate。`ordo.jdbc.migrate=false` 时不执行。表包括流程头 `ordo_process`、按 `(id, version)` 存储的定义/步骤(含 `parent_step_id` / `branch_id`)/候选人/转移、实例(含 `definition_version`)、任务、并行令牌 `ordo_instance_token`、`ordo_process_event`、`ordo_action_execution`。
|
||||
|
||||
自定义方言:实现 `SqlDialect` 并用 `META-INF/services` 注册,或提供 `SqlDialect` Bean。新增列时每个已支持方言目录各加一条迁移。
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -34,6 +41,9 @@ public interface OrdoEngine {
|
||||
/** Claims and processes up to {@code limit} overdue pending tasks. {@code limit} must be positive. */
|
||||
int processDue(int limit);
|
||||
|
||||
/** Earliest {@code dueAt} among pending tasks, or empty if none are scheduled. */
|
||||
Optional<Instant> nextDueAt();
|
||||
|
||||
default ProcessInstance withdraw(String instanceId, String actor) {
|
||||
return withdraw(instanceId, actor, null);
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
|
||||
/**
|
||||
* Decision rule applied when an approval step has more than one candidate.
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/**
|
||||
* A named step in a process definition. Approval steps have one or more candidate assignees;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
/** What happens when an approval step's due time is reached. */
|
||||
public enum DueThen {
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** One branch inside a {@link StepKind#PARALLEL} block. The first step id is the branch start. */
|
||||
public record ParallelBranch(String id, List<String> stepIds) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
/** Locates an inner step inside a {@link StepKind#PARALLEL} block. */
|
||||
public record ParallelMembership(ApprovalStep parallel, ParallelBranch branch) {
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -9,6 +9,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Immutable blueprint for an approval process with explicit step transitions. */
|
||||
public record ProcessDefinition(String id, int version, String name, List<ApprovalStep> steps,
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -15,6 +15,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.jetlumen.ordo.api.util.Jsons;
|
||||
|
||||
/** Parses a structural JSON process graph into a {@link ProcessDefinition}. */
|
||||
public final class ProcessDefinitionParser {
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
@@ -11,6 +11,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Closed-set boolean tree evaluated against {@link ProcessContext} variables. */
|
||||
public sealed interface RoutingPredicate permits RoutingPredicate.Compare, RoutingPredicate.In,
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@@ -9,6 +9,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.util.Jsons;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/**
|
||||
* Outgoing-edge condition: either a built-in {@link RoutingPredicate} or a host
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Relative SLA on an approval step. */
|
||||
public record StepDue(Duration after, DueThen then, String to, String action) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
public enum StepKind {
|
||||
APPROVAL, ACTION, PARALLEL
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.definition;
|
||||
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/**
|
||||
* An outgoing edge from an approval step. When {@link #toStepId()} is {@code null}, matching this
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.query;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.query;
|
||||
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
@@ -43,6 +43,9 @@ public interface ApprovalTaskRepository {
|
||||
|
||||
List<ApprovalTask> findDuePending(Instant now, int limit);
|
||||
|
||||
/** Earliest {@code dueAt} among pending tasks, or empty if none are scheduled. */
|
||||
Optional<Instant> findNextDueAt();
|
||||
|
||||
boolean claimIfDue(String taskId, String expectedAssignee, Instant now);
|
||||
|
||||
/** Paginated, filterable query; results are ordered newest-first (created_at desc). */
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.InstanceToken;
|
||||
import com.jetlumen.ordo.api.runtime.InstanceToken;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Persisted record of an ACTION step invocation. */
|
||||
public record ActionExecution(String id, String instanceId, String stepId, String actionKey,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
/** Lifecycle of a persisted ACTION-step execution. */
|
||||
public enum ActionExecutionStatus {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Runtime token for one branch of a {@link StepKind#PARALLEL} block. */
|
||||
public record InstanceToken(String id, String instanceId, String parallelStepId, String branchId, String currentStepId,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Immutable audit record of something that happened to a process instance. */
|
||||
public record ProcessEvent(String id, String instanceId, String taskId, String stepId, ProcessEventType type,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
/** Kinds of append-only process history events. */
|
||||
public enum ProcessEventType {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/**
|
||||
* Read-only engine metadata plus business {@link ProcessContext} for host SPIs.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
public enum ProcessStatus {
|
||||
RUNNING, APPROVED, REJECTED, WITHDRAWN, CANCELLED
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Immutable audit record created when an approval task is completed. */
|
||||
public record TaskAction(String actor, String comment, Instant operatedAt) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
public enum TaskStatus {
|
||||
PENDING, APPROVED, REJECTED,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.runtime;
|
||||
|
||||
/** Status of a parallel-branch token. */
|
||||
public enum TokenStatus {
|
||||
+5
-2
@@ -1,9 +1,12 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
/**
|
||||
* Executes a named action step against the running instance. Hosts supply a
|
||||
* singleton implementation (same pattern as {@link RoutingCondition}); the database only stores
|
||||
* the {@code actionKey} string.
|
||||
* the {@code actionKey} string. Prefer {@link NamedAction} beans; Spring wires a dispatcher
|
||||
* when this facade is not defined.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ActionHandler {
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
/**
|
||||
* Resolves the actual assignee for a single candidate of an approval step when its task is
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import java.util.List;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
/**
|
||||
* A single ACTION implementation keyed by {@link #key()}, matching {@code action} on the step
|
||||
* (and {@code due.then.notify} keys). Spring collects these beans into a dispatching
|
||||
* {@link ActionHandler} when the host does not define that facade itself.
|
||||
*/
|
||||
public interface NamedAction {
|
||||
String key();
|
||||
|
||||
void execute(ProcessRuntime runtime);
|
||||
|
||||
default String label() {
|
||||
return key();
|
||||
}
|
||||
|
||||
default List<OrdoCatalog.Param> params() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default OrdoCatalog.Item catalogItem() {
|
||||
return new OrdoCatalog.Item(key(), label(), params());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
/**
|
||||
* A single routing condition keyed by {@link #key()}, matching {@code when.ref}. Spring collects
|
||||
* these beans into a dispatching {@link RoutingCondition} when the host does not define that
|
||||
* facade itself.
|
||||
*/
|
||||
public interface NamedCondition {
|
||||
String key();
|
||||
|
||||
boolean matches(Map<String, Object> args, ProcessRuntime runtime);
|
||||
|
||||
default String label() {
|
||||
return key();
|
||||
}
|
||||
|
||||
default List<OrdoCatalog.Param> params() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default OrdoCatalog.Item catalogItem() {
|
||||
return new OrdoCatalog.Item(key(), label(), params());
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
/** Host-registered keys for designers and REST dropdowns; not used by the engine at runtime. */
|
||||
public interface OrdoCatalog {
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
|
||||
/**
|
||||
* Host hook invoked after a process mutation has been committed. Implementations must not throw
|
||||
+4
-2
@@ -1,11 +1,13 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.spi;
|
||||
|
||||
import java.util.Map;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
/**
|
||||
* Evaluates a named routing condition ({@code when.ref}) against the running instance.
|
||||
* Hosts supply a singleton implementation (same pattern as {@link AssigneeResolver}); the
|
||||
* definition stores the {@code ref} key and optional {@code args}.
|
||||
* definition stores the {@code ref} key and optional {@code args}. Prefer {@link NamedCondition}
|
||||
* beans; Spring wires a dispatcher when this facade is not defined.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RoutingCondition {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.api;
|
||||
package com.jetlumen.ordo.api.util;
|
||||
|
||||
/** Shared blank-string checks used by domain records and the engine. */
|
||||
public final class Texts {
|
||||
@@ -8,6 +8,13 @@ import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.definition.StepDue;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
|
||||
class ProcessDefinitionParserTest {
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@ import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.RoutingWhen;
|
||||
import com.jetlumen.ordo.api.definition.StepDue;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
|
||||
class ProcessDefinitionTest {
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import com.jetlumen.ordo.api.definition.RoutingWhen;
|
||||
|
||||
class RoutingWhenTest {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.api.query;
|
||||
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
package com.jetlumen.ordo.core;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.spi.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.RoutingWhen;
|
||||
import com.jetlumen.ordo.api.StepDue;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.TaskAction;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.spi.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.definition.RoutingWhen;
|
||||
import com.jetlumen.ordo.api.definition.StepDue;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
import com.jetlumen.ordo.api.runtime.TaskAction;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import com.jetlumen.ordo.api.TransactionExecutor;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
@@ -36,10 +36,10 @@ import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
import com.jetlumen.ordo.api.InstanceToken;
|
||||
import com.jetlumen.ordo.api.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.ParallelMembership;
|
||||
import com.jetlumen.ordo.api.TokenStatus;
|
||||
import com.jetlumen.ordo.api.runtime.InstanceToken;
|
||||
import com.jetlumen.ordo.api.definition.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.definition.ParallelMembership;
|
||||
import com.jetlumen.ordo.api.runtime.TokenStatus;
|
||||
import com.jetlumen.ordo.api.repository.ActionExecutionRepository;
|
||||
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
|
||||
import com.jetlumen.ordo.api.repository.InstanceTokenRepository;
|
||||
@@ -59,7 +59,9 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static com.jetlumen.ordo.api.Texts.requireText;
|
||||
import static com.jetlumen.ordo.api.util.Texts.requireText;
|
||||
|
||||
import com.jetlumen.ordo.core.routing.RoutingPredicateEvaluator;
|
||||
|
||||
/**
|
||||
* Repository-backed implementation of the approval runtime with explicit step transitions.
|
||||
@@ -343,6 +345,11 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
return processed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<Instant> nextDueAt() {
|
||||
return taskRepository.findNextDueAt();
|
||||
}
|
||||
|
||||
private boolean escalateDueTask(ApprovalTask overdue, Instant now, List<PendingAction> queued,
|
||||
List<ProcessEvent> events) {
|
||||
if (!taskRepository.claimIfDue(overdue.id(), overdue.assignee(), now)) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.jetlumen.ordo.core;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.spi.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
@@ -23,6 +23,7 @@ import com.jetlumen.ordo.core.repository.InMemoryProcessHistoryRepository;
|
||||
import com.jetlumen.ordo.core.repository.InMemoryProcessInstanceRepository;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -181,4 +182,9 @@ public final class InMemoryOrdoEngine implements OrdoEngine {
|
||||
public int processDue(int limit) {
|
||||
return delegate.processDue(limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> nextDueAt() {
|
||||
return delegate.nextDueAt();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ActionExecutionRepository;
|
||||
|
||||
+12
-3
@@ -1,8 +1,8 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
@@ -113,6 +113,15 @@ public final class InMemoryApprovalTaskRepository implements ApprovalTaskReposit
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<Instant> findNextDueAt() {
|
||||
return tasks.values().stream()
|
||||
.filter(task -> task.status() == TaskStatus.PENDING)
|
||||
.map(ApprovalTask::dueAt)
|
||||
.filter(Objects::nonNull)
|
||||
.min(Comparator.naturalOrder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean claimIfDue(String taskId, String expectedAssignee, Instant now) {
|
||||
ApprovalTask current = tasks.get(taskId);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.InstanceToken;
|
||||
import com.jetlumen.ordo.api.TokenStatus;
|
||||
import com.jetlumen.ordo.api.runtime.InstanceToken;
|
||||
import com.jetlumen.ordo.api.runtime.TokenStatus;
|
||||
import com.jetlumen.ordo.api.repository.InstanceTokenRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
package com.jetlumen.ordo.core;
|
||||
package com.jetlumen.ordo.core.routing;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.And;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.Compare;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.In;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.Not;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.Op;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate.Or;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.And;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.Compare;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.In;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.Not;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.Op;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate.Or;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** {@link ActionHandler} that dispatches by {@link NamedAction#key()}. */
|
||||
public final class DispatchingActionHandler implements ActionHandler {
|
||||
private final Map<String, NamedAction> actions;
|
||||
|
||||
private DispatchingActionHandler(Map<String, NamedAction> actions) {
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
public static DispatchingActionHandler of(List<? extends NamedAction> actions) {
|
||||
return new DispatchingActionHandler(NamedHandlers.indexByKey(actions, NamedAction::key, "action"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String actionKey, ProcessRuntime runtime) {
|
||||
NamedAction action = actions.get(NamedHandlers.lookupKey(actionKey));
|
||||
if (action == null) {
|
||||
throw new IllegalArgumentException("unknown action: " + actionKey);
|
||||
}
|
||||
action.execute(runtime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** {@link RoutingCondition} that dispatches by {@link NamedCondition#key()}. Unknown keys are false. */
|
||||
public final class DispatchingRoutingCondition implements RoutingCondition {
|
||||
private final Map<String, NamedCondition> conditions;
|
||||
|
||||
private DispatchingRoutingCondition(Map<String, NamedCondition> conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public static DispatchingRoutingCondition of(List<? extends NamedCondition> conditions) {
|
||||
return new DispatchingRoutingCondition(NamedHandlers.indexByKey(conditions, NamedCondition::key, "condition"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String conditionKey, Map<String, Object> args, ProcessRuntime runtime) {
|
||||
NamedCondition condition = conditions.get(NamedHandlers.lookupKey(conditionKey));
|
||||
if (condition == null) {
|
||||
return false;
|
||||
}
|
||||
return condition.matches(args, runtime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.util.Texts;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class NamedHandlers {
|
||||
private NamedHandlers() {
|
||||
}
|
||||
|
||||
static String requireKey(String key) {
|
||||
Texts.requireText(key, "key");
|
||||
return key.strip();
|
||||
}
|
||||
|
||||
static String lookupKey(String key) {
|
||||
return key == null ? "" : key.strip();
|
||||
}
|
||||
|
||||
static <T> Map<String, T> indexByKey(List<? extends T> items, Function<T, String> keyExtractor, String kind) {
|
||||
Objects.requireNonNull(items, kind + "s must not be null");
|
||||
Map<String, T> map = new LinkedHashMap<>();
|
||||
for (T item : items) {
|
||||
Objects.requireNonNull(item, kind + " must not be null");
|
||||
String key = requireKey(keyExtractor.apply(item));
|
||||
if (map.put(key, item) != null) {
|
||||
throw new IllegalStateException("duplicate " + kind + " key: " + key);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoCatalog;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Catalog projected from registered {@link NamedCondition} and {@link NamedAction} instances. */
|
||||
public final class RegistryOrdoCatalog implements OrdoCatalog {
|
||||
private final List<Item> conditions;
|
||||
private final List<Item> actions;
|
||||
|
||||
public RegistryOrdoCatalog(List<? extends NamedCondition> conditions, List<? extends NamedAction> actions) {
|
||||
this.conditions = Objects.requireNonNull(conditions, "conditions must not be null").stream()
|
||||
.map(NamedCondition::catalogItem)
|
||||
.toList();
|
||||
this.actions = Objects.requireNonNull(actions, "actions must not be null").stream()
|
||||
.map(NamedAction::catalogItem)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> conditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> actions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> assignees() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
package com.jetlumen.ordo.core;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.StepDue;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.spi.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.definition.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.definition.StepDue;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.core;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.RoutingPredicate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
@@ -9,6 +9,7 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import com.jetlumen.ordo.core.routing.RoutingPredicateEvaluator;
|
||||
|
||||
class RoutingPredicateEvaluatorTest {
|
||||
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.jetlumen.ordo.core.repository;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DispatchingHandlersTest {
|
||||
|
||||
private static final ProcessRuntime RUNTIME =
|
||||
new ProcessRuntime("inst", "def", 1, "alice", "step", ProcessContext.empty());
|
||||
|
||||
@Test
|
||||
void actionDispatchesByKey() {
|
||||
List<String> executed = new ArrayList<>();
|
||||
DispatchingActionHandler.of(List.of(action("mail", executed::add)))
|
||||
.execute("mail", RUNTIME);
|
||||
assertEquals(List.of("mail"), executed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void actionStripsKeyOnRegisterAndLookup() {
|
||||
List<String> executed = new ArrayList<>();
|
||||
DispatchingActionHandler.of(List.of(action(" mail ", executed::add)))
|
||||
.execute("mail", RUNTIME);
|
||||
assertEquals(List.of(" mail "), executed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownActionThrows() {
|
||||
DispatchingActionHandler handler = DispatchingActionHandler.of(List.of(action("mail", key -> {
|
||||
})));
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> handler.execute("other", RUNTIME));
|
||||
assertEquals("unknown action: other", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateActionKeyFailsFast() {
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> DispatchingActionHandler.of(List.of(action("mail", key -> {
|
||||
}), action("mail", key -> {
|
||||
}))));
|
||||
assertEquals("duplicate action key: mail", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankActionKeyRejected() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> DispatchingActionHandler.of(List.of(action(" ", key -> {
|
||||
}))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void conditionDispatchesByKey() {
|
||||
DispatchingRoutingCondition condition = DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> ((Number) args.get("threshold")).intValue() < 10)));
|
||||
assertTrue(condition.matches("amountGt", Map.of("threshold", 5), RUNTIME));
|
||||
assertFalse(condition.matches("amountGt", Map.of("threshold", 20), RUNTIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownConditionIsFalse() {
|
||||
DispatchingRoutingCondition condition = DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> true)));
|
||||
assertFalse(condition.matches("missing", Map.of(), RUNTIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateConditionKeyFailsFast() {
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> true),
|
||||
namedCondition("amountGt", (args, runtime) -> false))));
|
||||
assertEquals("duplicate condition key: amountGt", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankConditionKeyRejected() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> DispatchingRoutingCondition.of(List.of(namedCondition("", (args, runtime) -> true))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void catalogProjectsNamedHandlers() {
|
||||
RegistryOrdoCatalog catalog = new RegistryOrdoCatalog(
|
||||
List.of(namedCondition("amountGt", (args, runtime) -> true)),
|
||||
List.of(action("mail", key -> {
|
||||
})));
|
||||
assertEquals("amountGt", catalog.conditions().get(0).key());
|
||||
assertEquals("mail", catalog.actions().get(0).key());
|
||||
assertTrue(catalog.assignees().isEmpty());
|
||||
}
|
||||
|
||||
private static NamedAction action(String key, Consumer<String> onExecute) {
|
||||
return new NamedAction() {
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
onExecute.accept(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static NamedCondition namedCondition(String key, BiFunction<Map<String, Object>, ProcessRuntime, Boolean> delegate) {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return delegate.apply(args, runtime);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.jetlumen.ordo.example;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.core.InMemoryOrdoEngine;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
package com.jetlumen.ordo.spring;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinitionParser;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.io.Resource;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
|
||||
import java.lang.System.Logger;
|
||||
import java.lang.System.Logger.Level;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Optional scheduler that drains {@link OrdoEngine#processDue(int)} when {@code ordo.due.poll-ms} is positive. */
|
||||
public final class OrdoDuePoller implements SmartLifecycle {
|
||||
private static final Logger LOG = System.getLogger("ordo");
|
||||
|
||||
private final OrdoEngine ordoEngine;
|
||||
private final Clock clock;
|
||||
private final long pollMs;
|
||||
private final int batchSize;
|
||||
private ScheduledExecutorService executor;
|
||||
private ScheduledFuture<?> scheduled;
|
||||
private volatile boolean running;
|
||||
private volatile boolean ticking;
|
||||
|
||||
public OrdoDuePoller(OrdoEngine ordoEngine, Clock clock, long pollMs, int batchSize) {
|
||||
this.ordoEngine = Objects.requireNonNull(ordoEngine, "ordoEngine must not be null");
|
||||
this.clock = Objects.requireNonNull(clock, "clock must not be null");
|
||||
this.pollMs = pollMs;
|
||||
if (batchSize <= 0) {
|
||||
throw new IllegalArgumentException("batch size must be positive");
|
||||
}
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return pollMs > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (pollMs <= 0 || running) {
|
||||
return;
|
||||
}
|
||||
executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "ordo-due-poll");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
running = true;
|
||||
schedule(0);
|
||||
}
|
||||
|
||||
void wake() {
|
||||
if (!running || ticking) {
|
||||
return;
|
||||
}
|
||||
schedule(0);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
ticking = true;
|
||||
try {
|
||||
int processed;
|
||||
do {
|
||||
processed = ordoEngine.processDue(batchSize);
|
||||
} while (processed == batchSize);
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(Level.WARNING, "processDue failed", e);
|
||||
} finally {
|
||||
if (running) {
|
||||
schedule(nextDelayMs());
|
||||
}
|
||||
ticking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private long nextDelayMs() {
|
||||
Instant now = clock.instant();
|
||||
return ordoEngine.nextDueAt()
|
||||
.map(dueAt -> Math.min(Math.max(0L, dueAt.toEpochMilli() - now.toEpochMilli()), pollMs))
|
||||
.orElse(pollMs);
|
||||
}
|
||||
|
||||
private synchronized void schedule(long delayMs) {
|
||||
if (!running || executor == null) {
|
||||
return;
|
||||
}
|
||||
if (scheduled != null) {
|
||||
scheduled.cancel(false);
|
||||
}
|
||||
scheduled = executor.schedule(this::tick, delayMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
executor = null;
|
||||
}
|
||||
scheduled = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.spi.OrdoEventListener;
|
||||
|
||||
/** Forwards {@code TASK_CREATED} to {@link OrdoDuePoller} after the engine listener snapshot is taken. */
|
||||
public final class OrdoDueWakeBridge implements OrdoEventListener {
|
||||
private volatile OrdoDuePoller poller;
|
||||
|
||||
public void attach(OrdoDuePoller poller) {
|
||||
this.poller = poller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if (event.type() != ProcessEventType.TASK_CREATED) {
|
||||
return;
|
||||
}
|
||||
OrdoDuePoller current = poller;
|
||||
if (current != null) {
|
||||
current.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-1
@@ -51,9 +51,12 @@ public class OrdoProperties {
|
||||
}
|
||||
|
||||
public static class Due {
|
||||
/** Poll interval in milliseconds. {@code 0} disables scheduling. */
|
||||
/** Enables scheduling when positive; also the maximum idle sleep in milliseconds. */
|
||||
private long pollMs;
|
||||
|
||||
/** Tasks claimed per {@code processDue} call while draining. */
|
||||
private int batchSize = 100;
|
||||
|
||||
public long getPollMs() {
|
||||
return pollMs;
|
||||
}
|
||||
@@ -61,12 +64,26 @@ public class OrdoProperties {
|
||||
public void setPollMs(long pollMs) {
|
||||
this.pollMs = pollMs;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
public void setBatchSize(int batchSize) {
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Jdbc {
|
||||
/** Explicit dialect id ({@code postgresql}, {@code mysql}). Empty means detect from the DataSource. */
|
||||
private String dialect;
|
||||
|
||||
/** When true, Ordo runs its own Flyway against dialect locations. */
|
||||
private boolean migrate = true;
|
||||
|
||||
/** Flyway history table used only by Ordo (not {@code flyway_schema_history}). */
|
||||
private String historyTable = "ordo_schema_history";
|
||||
|
||||
public String getDialect() {
|
||||
return dialect;
|
||||
}
|
||||
@@ -74,6 +91,23 @@ public class OrdoProperties {
|
||||
public void setDialect(String dialect) {
|
||||
this.dialect = dialect;
|
||||
}
|
||||
|
||||
public boolean isMigrate() {
|
||||
return migrate;
|
||||
}
|
||||
|
||||
public void setMigrate(boolean migrate) {
|
||||
this.migrate = migrate;
|
||||
}
|
||||
|
||||
public String getHistoryTable() {
|
||||
return historyTable;
|
||||
}
|
||||
|
||||
public void setHistoryTable(String historyTable) {
|
||||
this.historyTable = historyTable == null || historyTable.isBlank()
|
||||
? "ordo_schema_history" : historyTable.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Rest {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDatabaseInitializerDetector;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** Treats {@link OrdoSchemaMigrator} as database initialization for {@code @DependsOnDatabaseInitialization}. */
|
||||
public class OrdoDatabaseInitializerDetector extends AbstractBeansOfTypeDatabaseInitializerDetector {
|
||||
|
||||
@Override
|
||||
protected Set<Class<?>> getDatabaseInitializerBeanTypes() {
|
||||
return Set.of(OrdoSchemaMigrator.class);
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
|
||||
import java.lang.System.Logger;
|
||||
import java.lang.System.Logger.Level;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Optional poller that calls {@link OrdoEngine#processDue(int)} when {@code ordo.due.poll-ms} is positive. */
|
||||
public final class OrdoDuePoller implements SmartLifecycle {
|
||||
private static final Logger LOG = System.getLogger("ordo");
|
||||
private static final int BATCH_SIZE = 100;
|
||||
|
||||
private final OrdoEngine ordoEngine;
|
||||
private final long pollMs;
|
||||
private ScheduledExecutorService executor;
|
||||
private volatile boolean running;
|
||||
|
||||
public OrdoDuePoller(OrdoEngine ordoEngine, long pollMs) {
|
||||
this.ordoEngine = Objects.requireNonNull(ordoEngine, "ordoEngine must not be null");
|
||||
this.pollMs = pollMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return pollMs > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (pollMs <= 0 || running) {
|
||||
return;
|
||||
}
|
||||
executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "ordo-due-poll");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
executor.scheduleWithFixedDelay(this::tick, pollMs, pollMs, TimeUnit.MILLISECONDS);
|
||||
running = true;
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
try {
|
||||
ordoEngine.processDue(BATCH_SIZE);
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(Level.WARNING, "processDue failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
executor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
}
|
||||
+10
-92
@@ -1,34 +1,22 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.configuration.FluentConfiguration;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* Points Flyway at the dialect migration directory before Flyway runs.
|
||||
* Host {@code spring.flyway.locations} is left unchanged when set.
|
||||
*
|
||||
* <p>Does not inject {@link DataSource} or {@link SqlDialect} at bean-creation time
|
||||
* (that would cycle with DataSource → Flyway → customizer). Dialect is resolved inside
|
||||
* {@code customize} from the FluentConfiguration DataSource / {@code ordo.jdbc.dialect}.
|
||||
*
|
||||
* <p>FlywayConfigurationCustomizer moved between Boot 3 and Boot 4; a reflective
|
||||
* {@link FactoryBean} supplies a proxy for whichever type is on the classpath.
|
||||
* Migrates Ordo tables with a dedicated Flyway history table, independent of
|
||||
* {@code spring.flyway}. Runs after the host {@code flywayInitializer} when present so a
|
||||
* non-empty schema does not break host migrate.
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@@ -36,89 +24,19 @@ import java.lang.reflect.Proxy;
|
||||
@ConditionalOnBean(DataSource.class)
|
||||
@AutoConfigureAfter(name = {
|
||||
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration"
|
||||
})
|
||||
@AutoConfigureBefore(name = {
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"
|
||||
})
|
||||
@EnableConfigurationProperties(OrdoProperties.class)
|
||||
public class OrdoFlywayAutoConfiguration {
|
||||
|
||||
private static final String[] FLYWAY_CUSTOMIZER_TYPES = {
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayConfigurationCustomizer",
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer"
|
||||
};
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.flywaydb.core.api.configuration.FluentConfiguration")
|
||||
public FactoryBean<Object> ordoFlywayConfigurationCustomizer(
|
||||
OrdoProperties ordoProperties, Environment environment) {
|
||||
Class<?> customizerType = resolveFlywayCustomizerType();
|
||||
if (customizerType == null) {
|
||||
return null;
|
||||
}
|
||||
return new FlywayLocationsCustomizerFactoryBean(customizerType, ordoProperties, environment);
|
||||
}
|
||||
|
||||
static Class<?> resolveFlywayCustomizerType() {
|
||||
ClassLoader classLoader = OrdoFlywayAutoConfiguration.class.getClassLoader();
|
||||
for (String name : FLYWAY_CUSTOMIZER_TYPES) {
|
||||
try {
|
||||
return Class.forName(name, false, classLoader);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// Boot 3 vs Boot 4
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class FlywayLocationsCustomizerFactoryBean implements FactoryBean<Object> {
|
||||
private final Class<?> customizerType;
|
||||
private final OrdoProperties ordoProperties;
|
||||
private final Environment environment;
|
||||
|
||||
private FlywayLocationsCustomizerFactoryBean(Class<?> customizerType, OrdoProperties ordoProperties,
|
||||
Environment environment) {
|
||||
this.customizerType = customizerType;
|
||||
this.ordoProperties = ordoProperties;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject() {
|
||||
return Proxy.newProxyInstance(customizerType.getClassLoader(), new Class<?>[] {customizerType},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("customize".equals(name) && args != null && args.length == 1) {
|
||||
applyLocations((FluentConfiguration) args[0]);
|
||||
return null;
|
||||
}
|
||||
if ("equals".equals(name)) {
|
||||
return proxy == args[0];
|
||||
}
|
||||
if ("hashCode".equals(name)) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if ("toString".equals(name)) {
|
||||
return "OrdoFlywayLocationsCustomizer";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void applyLocations(FluentConfiguration configuration) {
|
||||
if (environment.containsProperty("spring.flyway.locations")) {
|
||||
return;
|
||||
}
|
||||
SqlDialect dialect = SqlDialects.resolve(configuration.getDataSource(),
|
||||
ordoProperties.getJdbc().getDialect());
|
||||
configuration.locations(dialect.flywayLocations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return customizerType;
|
||||
public OrdoSchemaMigrator ordoSchemaMigrator(DataSource dataSource, OrdoProperties ordoProperties,
|
||||
BeanFactory beanFactory) {
|
||||
if (beanFactory.containsBean("flywayInitializer")) {
|
||||
beanFactory.getBean("flywayInitializer");
|
||||
}
|
||||
return new OrdoSchemaMigrator(dataSource, ordoProperties);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-10
@@ -1,10 +1,12 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.spi.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.TransactionExecutor;
|
||||
import com.jetlumen.ordo.api.repository.ActionExecutionRepository;
|
||||
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
|
||||
@@ -13,6 +15,11 @@ import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
|
||||
import com.jetlumen.ordo.core.DefaultOrdoEngine;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingActionHandler;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingRoutingCondition;
|
||||
import com.jetlumen.ordo.spring.OrdoDefinitionLoader;
|
||||
import com.jetlumen.ordo.spring.OrdoDuePoller;
|
||||
import com.jetlumen.ordo.spring.OrdoDueWakeBridge;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
|
||||
@@ -37,6 +44,7 @@ import org.springframework.context.annotation.Bean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
|
||||
@@ -72,14 +80,16 @@ public class OrdoJdbcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RoutingCondition ordoRoutingCondition() {
|
||||
return RoutingCondition.always();
|
||||
public RoutingCondition ordoRoutingCondition(ObjectProvider<NamedCondition> namedConditions) {
|
||||
List<NamedCondition> conditions = namedConditions.orderedStream().toList();
|
||||
return conditions.isEmpty() ? RoutingCondition.always() : DispatchingRoutingCondition.of(conditions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ActionHandler ordoActionHandler() {
|
||||
return ActionHandler.noop();
|
||||
public ActionHandler ordoActionHandler(ObjectProvider<NamedAction> namedActions) {
|
||||
List<NamedAction> actions = namedActions.orderedStream().toList();
|
||||
return actions.isEmpty() ? ActionHandler.noop() : DispatchingActionHandler.of(actions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -143,6 +153,12 @@ public class OrdoJdbcAutoConfiguration {
|
||||
return new JdbcInstanceTokenRepository(connectionProvider, ordoSqlDialect);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoDueWakeBridge ordoDueWakeBridge() {
|
||||
return new OrdoDueWakeBridge();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoEngine ordoEngine(Clock ordoClock,
|
||||
@@ -172,7 +188,11 @@ public class OrdoJdbcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoDuePoller ordoDuePoller(OrdoEngine ordoEngine, OrdoProperties ordoProperties) {
|
||||
return new OrdoDuePoller(ordoEngine, ordoProperties.getDue().getPollMs());
|
||||
public OrdoDuePoller ordoDuePoller(OrdoEngine ordoEngine, Clock ordoClock, OrdoProperties ordoProperties,
|
||||
OrdoDueWakeBridge ordoDueWakeBridge) {
|
||||
OrdoDuePoller poller = new OrdoDuePoller(ordoEngine, ordoClock, ordoProperties.getDue().getPollMs(),
|
||||
ordoProperties.getDue().getBatchSize());
|
||||
ordoDueWakeBridge.attach(poller);
|
||||
return poller;
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.MigrationInfo;
|
||||
import org.flywaydb.core.api.MigrationVersion;
|
||||
import org.flywaydb.core.api.configuration.FluentConfiguration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Runs Ordo Flyway scripts independently of the host {@code spring.flyway} instance.
|
||||
* Presence of this bean marks Ordo schema initialization for
|
||||
* {@code @DependsOnDatabaseInitialization}.
|
||||
*/
|
||||
public final class OrdoSchemaMigrator {
|
||||
|
||||
private static final String PROCESS_TABLE = "ordo_process";
|
||||
|
||||
public OrdoSchemaMigrator(DataSource dataSource, OrdoProperties properties) {
|
||||
OrdoProperties.Jdbc jdbc = properties.getJdbc();
|
||||
if (!jdbc.isMigrate()) {
|
||||
return;
|
||||
}
|
||||
String[] locations = SqlDialects.resolve(dataSource, jdbc.getDialect()).flywayLocations();
|
||||
String historyTable = jdbc.getHistoryTable();
|
||||
FluentConfiguration configuration = new FluentConfiguration(OrdoSchemaMigrator.class.getClassLoader())
|
||||
.dataSource(dataSource)
|
||||
.locations(locations)
|
||||
.table(historyTable);
|
||||
applyBaselineIfNeeded(configuration, dataSource, historyTable);
|
||||
configuration.load().migrate();
|
||||
}
|
||||
|
||||
private static void applyBaselineIfNeeded(FluentConfiguration configuration, DataSource dataSource,
|
||||
String historyTable) {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
if (tableExists(metaData, connection, historyTable)) {
|
||||
return;
|
||||
}
|
||||
// Host tables / flyway_schema_history make the schema non-empty; Flyway then requires
|
||||
// baseline before migrate. Existing Ordo tables baseline to latest (skip scripts);
|
||||
// otherwise baseline at 0 so V1+ still run.
|
||||
if (tableExists(metaData, connection, PROCESS_TABLE)) {
|
||||
MigrationVersion latest = latestVersion(configuration.load());
|
||||
configuration.baselineOnMigrate(true).baselineVersion(latest);
|
||||
} else {
|
||||
configuration.baselineOnMigrate(true).baselineVersion(MigrationVersion.fromVersion("0"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("failed to inspect schema before Ordo Flyway migrate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean tableExists(DatabaseMetaData metaData, Connection connection, String table)
|
||||
throws SQLException {
|
||||
String catalog = connection.getCatalog();
|
||||
String schema = connection.getSchema();
|
||||
String[] names = {table, table.toLowerCase(Locale.ROOT), table.toUpperCase(Locale.ROOT)};
|
||||
for (String name : names) {
|
||||
try (ResultSet tables = metaData.getTables(catalog, schema, name, new String[] {"TABLE"})) {
|
||||
if (tables.next()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MigrationVersion latestVersion(Flyway flyway) {
|
||||
MigrationVersion latest = MigrationVersion.fromVersion("0");
|
||||
for (MigrationInfo info : flyway.info().all()) {
|
||||
if (info.getVersion() != null && info.getVersion().compareTo(latest) > 0) {
|
||||
latest = info.getVersion();
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -2,9 +2,9 @@ package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.jetlumen.ordo.api.Jsons;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.util.Jsons;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinitionParser;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.spi.OrdoCatalog;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessContext;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
+15
-3
@@ -1,9 +1,13 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.core.spi.RegistryOrdoCatalog;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
@@ -15,6 +19,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(DispatcherServlet.class)
|
||||
@ConditionalOnProperty(prefix = "ordo.rest", name = "enabled", havingValue = "true")
|
||||
@@ -33,7 +39,13 @@ public class OrdoRestAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoCatalog ordoCatalog() {
|
||||
return OrdoCatalog.empty();
|
||||
public OrdoCatalog ordoCatalog(ObjectProvider<NamedAction> namedActions,
|
||||
ObjectProvider<NamedCondition> namedConditions) {
|
||||
List<NamedAction> actions = namedActions.orderedStream().toList();
|
||||
List<NamedCondition> conditions = namedConditions.orderedStream().toList();
|
||||
if (actions.isEmpty() && conditions.isEmpty()) {
|
||||
return OrdoCatalog.empty();
|
||||
}
|
||||
return new RegistryOrdoCatalog(conditions, actions);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.runtime.TaskStatus;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.jetlumen.ordo.spring.jdbc.OrdoDatabaseInitializerDetector
|
||||
+107
-11
@@ -1,18 +1,24 @@
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.spi.ActionHandler;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.spi.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.spi.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingActionHandler;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingRoutingCondition;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
@@ -23,10 +29,12 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import com.jetlumen.ordo.spring.OrdoDefinitionLoader;
|
||||
|
||||
class OrdoJdbcAutoConfigurationTest {
|
||||
|
||||
@@ -65,7 +73,7 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
|
||||
@Test
|
||||
void honoursExplicitMysqlDialect() {
|
||||
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "spring.flyway.enabled=false")
|
||||
withDataSourceRunner.withPropertyValues("ordo.jdbc.dialect=mysql", "ordo.jdbc.migrate=false")
|
||||
.run(context -> assertThat(context.getBean(SqlDialect.class).id()).isEqualTo("mysql"));
|
||||
}
|
||||
|
||||
@@ -168,6 +176,54 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsNoopAndAlwaysWhenNoNamedHandlers() {
|
||||
withDataSourceRunner.run(context -> {
|
||||
context.getBean(ActionHandler.class).execute("any", null);
|
||||
assertThat(context.getBean(RoutingCondition.class).matches("any", Map.of(), null)).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void assemblesDispatchingActionHandlerFromNamedActions() {
|
||||
withDataSourceRunner.withUserConfiguration(NamedActionConfig.class)
|
||||
.run(context -> {
|
||||
assertThat(context.getBean(ActionHandler.class)).isInstanceOf(DispatchingActionHandler.class);
|
||||
OrdoEngine engine = context.getBean(OrdoEngine.class);
|
||||
engine.publish(new ProcessDefinition("leave", "Leave",
|
||||
List.of(ApprovalStep.action("notify", "Notify", "leave-mail")),
|
||||
List.of(StepTransition.end("notify"))));
|
||||
engine.start("leave", "alice");
|
||||
assertThat(context.getBean(RecordingNamedAction.class).executed).containsExactly("leave-mail");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedActionHandlerOverNamedActions() {
|
||||
withDataSourceRunner.withUserConfiguration(CustomActionHandlerConfig.class, NamedActionConfig.class)
|
||||
.run(context -> assertThatThrownBy(() -> context.getBean(ActionHandler.class).execute("any", null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("custom-action"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assemblesDispatchingRoutingConditionFromNamedConditions() {
|
||||
withDataSourceRunner.withUserConfiguration(NamedConditionConfig.class)
|
||||
.run(context -> {
|
||||
RoutingCondition routing = context.getBean(RoutingCondition.class);
|
||||
assertThat(routing).isInstanceOf(DispatchingRoutingCondition.class);
|
||||
assertThat(routing.matches("amountGt", Map.of(), null)).isTrue();
|
||||
assertThat(routing.matches("other", Map.of(), null)).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedRoutingConditionOverNamedConditions() {
|
||||
withDataSourceRunner.withUserConfiguration(CustomRoutingConditionConfig.class, NamedConditionConfig.class)
|
||||
.run(context -> assertThat(context.getBean(RoutingCondition.class)
|
||||
.matches("other", Map.of(), null)).isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokesOrdoEventListenerBeans() {
|
||||
withDataSourceRunner.withUserConfiguration(RecordingListenerConfig.class)
|
||||
@@ -224,6 +280,46 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedActionConfig {
|
||||
@Bean
|
||||
RecordingNamedAction leaveMailAction() {
|
||||
return new RecordingNamedAction();
|
||||
}
|
||||
}
|
||||
|
||||
static class RecordingNamedAction implements NamedAction {
|
||||
final List<String> executed = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return "leave-mail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
executed.add(key());
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedConditionConfig {
|
||||
@Bean
|
||||
NamedCondition amountGt() {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "amountGt";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class RecordingListenerConfig {
|
||||
@Bean
|
||||
|
||||
+98
@@ -1,6 +1,10 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.spi.NamedAction;
|
||||
import com.jetlumen.ordo.api.spi.NamedCondition;
|
||||
import com.jetlumen.ordo.api.spi.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -12,11 +16,14 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
@@ -76,6 +83,37 @@ class OrdoRestAutoConfigurationTest {
|
||||
.hasSingleBean(OrdoCatalogController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectsCatalogFromNamedHandlers() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true")
|
||||
.withUserConfiguration(NamedCatalogConfig.class)
|
||||
.run(context -> {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
mvc.perform(get("/ordo/catalog/actions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("leave-mail"))
|
||||
.andExpect(jsonPath("$[0].label").value("Notify HR"));
|
||||
mvc.perform(get("/ordo/catalog/conditions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("amountGt"));
|
||||
mvc.perform(get("/ordo/catalog/assignees"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedCatalog() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true")
|
||||
.withUserConfiguration(NamedCatalogConfig.class, CustomCatalogConfig.class)
|
||||
.run(context -> {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
mvc.perform(get("/ordo/catalog/actions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("custom-action"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePublishStartApproveAndErrorMappings() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true").run(context -> {
|
||||
@@ -149,4 +187,64 @@ class OrdoRestAutoConfigurationTest {
|
||||
.andExpect(status().isBadRequest());
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedCatalogConfig {
|
||||
@Bean
|
||||
NamedAction leaveMail() {
|
||||
return new NamedAction() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "leave-mail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "Notify HR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
NamedCondition amountGt() {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "amountGt";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomCatalogConfig {
|
||||
@Bean
|
||||
OrdoCatalog ordoCatalog() {
|
||||
return new OrdoCatalog() {
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> conditions() {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> actions() {
|
||||
return java.util.List.of(new OrdoCatalog.Item("custom-action", "Custom"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> assignees() {
|
||||
return java.util.List.of();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ActionExecutionRepository;
|
||||
|
||||
+41
-2
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.runtime.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
@@ -11,6 +11,7 @@ import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalTaskMapper;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -18,6 +19,7 @@ import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -52,6 +54,8 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
private static final String SELECT_DUE_PENDING_BASE =
|
||||
"SELECT " + TASK_COLUMNS + " FROM ordo_approval_task WHERE status = 'PENDING' AND due_at IS NOT NULL"
|
||||
+ " AND due_at <= ? ORDER BY due_at, id";
|
||||
private static final String SELECT_NEXT_DUE_AT =
|
||||
"SELECT MIN(due_at) FROM ordo_approval_task WHERE status = 'PENDING' AND due_at IS NOT NULL";
|
||||
private static final String TASK_COLUMNS_QUALIFIED =
|
||||
"t.id, t.instance_id, t.step_id, t.task_name, t.assignee, t.status, t.created_at, t.completed_at,"
|
||||
+ " t.action_actor, t.action_comment, t.action_at, t.due_at";
|
||||
@@ -67,7 +71,7 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
public JdbcApprovalTaskRepository(JdbcConnectionProvider connectionProvider, SqlDialect dialect) {
|
||||
this.connectionProvider = Objects.requireNonNull(connectionProvider, "connectionProvider must not be null");
|
||||
this.dialect = Objects.requireNonNull(dialect, "dialect must not be null");
|
||||
this.selectDuePending = dialect.limit(SELECT_DUE_PENDING_BASE, false);
|
||||
this.selectDuePending = duePendingSql(this.connectionProvider, this.dialect);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -180,6 +184,23 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> findNextDueAt() {
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement select = connection.prepareStatement(SELECT_NEXT_DUE_AT);
|
||||
ResultSet resultSet = select.executeQuery()) {
|
||||
if (!resultSet.next()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Timestamp timestamp = resultSet.getTimestamp(1);
|
||||
return timestamp == null ? Optional.empty() : Optional.of(timestamp.toInstant());
|
||||
} catch (SQLException e) {
|
||||
throw new JdbcStorageException("failed to query next due at", e);
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claimIfDue(String taskId, String expectedAssignee, Instant now) {
|
||||
Objects.requireNonNull(taskId, "taskId must not be null");
|
||||
@@ -275,6 +296,24 @@ public final class JdbcApprovalTaskRepository implements ApprovalTaskRepository
|
||||
}
|
||||
}
|
||||
|
||||
private static String duePendingSql(JdbcConnectionProvider connectionProvider, SqlDialect dialect) {
|
||||
String limited = dialect.limit(SELECT_DUE_PENDING_BASE, false);
|
||||
return supportsSkipLocked(connectionProvider) ? dialect.forUpdateSkipLocked(limited) : limited;
|
||||
}
|
||||
|
||||
private static boolean supportsSkipLocked(JdbcConnectionProvider connectionProvider) {
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
String product = metaData.getDatabaseProductName();
|
||||
return product != null && !product.toLowerCase(Locale.ROOT).contains("h2");
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ApprovalTask> findAll(String sql, String parameter) {
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement select = connection.prepareStatement(sql)) {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.InstanceToken;
|
||||
import com.jetlumen.ordo.api.TokenStatus;
|
||||
import com.jetlumen.ordo.api.runtime.InstanceToken;
|
||||
import com.jetlumen.ordo.api.runtime.TokenStatus;
|
||||
import com.jetlumen.ordo.api.repository.InstanceTokenRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.definition.ParallelBranch;
|
||||
import com.jetlumen.ordo.api.definition.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.StepTransition;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.runtime.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
+5
@@ -16,4 +16,9 @@ abstract class LimitOffsetSqlDialect implements SqlDialect {
|
||||
public final String forUpdate(String sql) {
|
||||
return sql + " FOR UPDATE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String forUpdateSkipLocked(String sql) {
|
||||
return sql + " FOR UPDATE SKIP LOCKED";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,8 @@ public interface SqlDialect {
|
||||
|
||||
String forUpdate(String sql);
|
||||
|
||||
/** Appends {@code FOR UPDATE SKIP LOCKED}. */
|
||||
String forUpdateSkipLocked(String sql);
|
||||
|
||||
String[] flywayLocations();
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.jetlumen.ordo.storage.jdbc.mapper;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ActionExecutionStatus;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecution;
|
||||
import com.jetlumen.ordo.api.runtime.ActionExecutionStatus;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
+6
-5
@@ -1,18 +1,19 @@
|
||||
package com.jetlumen.ordo.storage.jdbc.mapper;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.DueThen;
|
||||
import com.jetlumen.ordo.api.StepDue;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.definition.DueThen;
|
||||
import com.jetlumen.ordo.api.definition.StepDue;
|
||||
import com.jetlumen.ordo.api.definition.StepKind;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import com.jetlumen.ordo.api.definition.ApprovalStep;
|
||||
|
||||
/**
|
||||
* Maps rows of {@code ordo_approval_step}. A step's candidate list lives in the separate
|
||||
* {@code ordo_step_candidate} table, so this only reads the scalar step columns; the repository
|
||||
* assembles the full {@link com.jetlumen.ordo.api.ApprovalStep} once candidates are loaded.
|
||||
* assembles the full {@link com.jetlumen.ordo.api.definition.ApprovalStep} once candidates are loaded.
|
||||
*/
|
||||
public final class ApprovalStepMapper {
|
||||
private ApprovalStepMapper() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user