refactor: extract shared Texts, Jsons, and SqlBindings helpers

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-18 11:20:34 +08:00
co-authored by Cursor
parent 1f6e33c14b
commit 0e9bb4bd16
25 changed files with 189 additions and 176 deletions
@@ -8,18 +8,12 @@ public record ActionExecution(String id, String instanceId, String stepId, Strin
ActionExecutionStatus status, String errorMessage, Instant startedAt, ActionExecutionStatus status, String errorMessage, Instant startedAt,
Instant finishedAt) { Instant finishedAt) {
public ActionExecution { public ActionExecution {
requireText(id, "id"); Texts.requireText(id, "id");
requireText(instanceId, "instanceId"); Texts.requireText(instanceId, "instanceId");
requireText(stepId, "stepId"); Texts.requireText(stepId, "stepId");
requireText(actionKey, "actionKey"); Texts.requireText(actionKey, "actionKey");
Objects.requireNonNull(status, "status must not be null"); Objects.requireNonNull(status, "status must not be null");
Objects.requireNonNull(startedAt, "startedAt must not be null"); Objects.requireNonNull(startedAt, "startedAt must not be null");
errorMessage = errorMessage == null || errorMessage.isBlank() ? null : errorMessage.strip(); errorMessage = Texts.blankToNull(errorMessage);
}
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
} }
} }
@@ -12,8 +12,8 @@ import java.util.Set;
public record ApprovalStep(String id, String name, List<String> candidates, ApprovalPolicy policy, StepKind kind, public record ApprovalStep(String id, String name, List<String> candidates, ApprovalPolicy policy, StepKind kind,
String actionKey, StepDue due) { String actionKey, StepDue due) {
public ApprovalStep { public ApprovalStep {
requireText(id, "step id"); Texts.requireText(id, "step id");
requireText(name, "step name"); Texts.requireText(name, "step name");
kind = kind == null ? StepKind.APPROVAL : kind; kind = kind == null ? StepKind.APPROVAL : kind;
Objects.requireNonNull(policy, "policy must not be null"); Objects.requireNonNull(policy, "policy must not be null");
Objects.requireNonNull(candidates, "candidates must not be null"); Objects.requireNonNull(candidates, "candidates must not be null");
@@ -22,7 +22,7 @@ public record ApprovalStep(String id, String name, List<String> candidates, Appr
if (!candidates.isEmpty()) { if (!candidates.isEmpty()) {
throw new IllegalArgumentException("an action step must not have candidates"); throw new IllegalArgumentException("an action step must not have candidates");
} }
requireText(actionKey, "action key"); Texts.requireText(actionKey, "action key");
actionKey = actionKey.strip(); actionKey = actionKey.strip();
if (due != null) { if (due != null) {
throw new IllegalArgumentException("an action step must not have due"); throw new IllegalArgumentException("an action step must not have due");
@@ -37,7 +37,7 @@ public record ApprovalStep(String id, String name, List<String> candidates, Appr
} }
Set<String> distinct = new HashSet<>(); Set<String> distinct = new HashSet<>();
for (String candidate : candidates) { for (String candidate : candidates) {
requireText(candidate, "candidate"); Texts.requireText(candidate, "candidate");
if (!distinct.add(candidate)) { if (!distinct.add(candidate)) {
throw new IllegalArgumentException("duplicate candidate in step " + id + ": " + candidate); throw new IllegalArgumentException("duplicate candidate in step " + id + ": " + candidate);
} }
@@ -62,10 +62,4 @@ public record ApprovalStep(String id, String name, List<String> candidates, Appr
public static ApprovalStep action(String id, String name, String actionKey) { public static ApprovalStep action(String id, String name, String actionKey) {
return new ApprovalStep(id, name, List.of(), ApprovalPolicy.ANY, StepKind.ACTION, actionKey); return new ApprovalStep(id, name, List.of(), ApprovalPolicy.ANY, StepKind.ACTION, actionKey);
} }
static void requireText(String value, String field) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(field + " must not be blank");
}
}
} }
@@ -0,0 +1,14 @@
package com.jetlumen.ordo.api;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
/** Shared Jackson mappers. */
public final class Jsons {
public static final ObjectMapper MAPPER = new ObjectMapper();
public static final ObjectMapper NON_NULL = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
private Jsons() {
}
}
@@ -29,9 +29,9 @@ public interface OrdoCatalog {
record Param(String name, String type, String label) { record Param(String name, String type, String label) {
public Param { public Param {
ApprovalStep.requireText(name, "param name"); Texts.requireText(name, "param name");
ApprovalStep.requireText(type, "param type"); Texts.requireText(type, "param type");
ApprovalStep.requireText(label, "param label"); Texts.requireText(label, "param label");
type = type.strip().toLowerCase(Locale.ROOT); type = type.strip().toLowerCase(Locale.ROOT);
if (!PARAM_TYPES.contains(type)) { if (!PARAM_TYPES.contains(type)) {
throw new IllegalArgumentException("param type must be number, string or boolean"); throw new IllegalArgumentException("param type must be number, string or boolean");
@@ -11,11 +11,11 @@ import java.util.Set;
public record ProcessDefinition(String id, int version, String name, List<ApprovalStep> steps, public record ProcessDefinition(String id, int version, String name, List<ApprovalStep> steps,
List<StepTransition> transitions) { List<StepTransition> transitions) {
public ProcessDefinition { public ProcessDefinition {
ApprovalStep.requireText(id, "definition id"); Texts.requireText(id, "definition id");
if (version < 0) { if (version < 0) {
throw new IllegalArgumentException("definition version must not be negative"); throw new IllegalArgumentException("definition version must not be negative");
} }
ApprovalStep.requireText(name, "definition name"); Texts.requireText(name, "definition name");
steps = List.copyOf(steps); steps = List.copyOf(steps);
if (steps.isEmpty()) { if (steps.isEmpty()) {
throw new IllegalArgumentException("a definition must contain at least one step"); throw new IllegalArgumentException("a definition must contain at least one step");
@@ -1,6 +1,5 @@
package com.jetlumen.ordo.api; package com.jetlumen.ordo.api;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -17,8 +16,7 @@ import java.util.Objects;
/** Parses a structural JSON process graph into a {@link ProcessDefinition}. */ /** Parses a structural JSON process graph into a {@link ProcessDefinition}. */
public final class ProcessDefinitionParser { public final class ProcessDefinitionParser {
private static final ObjectMapper MAPPER = new ObjectMapper() private static final ObjectMapper MAPPER = Jsons.NON_NULL;
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
private ProcessDefinitionParser() { private ProcessDefinitionParser() {
} }
@@ -7,23 +7,13 @@ import java.util.Objects;
public record ProcessEvent(String id, String instanceId, String taskId, String stepId, ProcessEventType type, public record ProcessEvent(String id, String instanceId, String taskId, String stepId, ProcessEventType type,
String actor, String detail, Instant occurredAt) { String actor, String detail, Instant occurredAt) {
public ProcessEvent { public ProcessEvent {
requireText(id, "id"); Texts.requireText(id, "id");
requireText(instanceId, "instanceId"); Texts.requireText(instanceId, "instanceId");
Objects.requireNonNull(type, "type must not be null"); Objects.requireNonNull(type, "type must not be null");
Objects.requireNonNull(occurredAt, "occurredAt must not be null"); Objects.requireNonNull(occurredAt, "occurredAt must not be null");
taskId = blankToNull(taskId); taskId = Texts.blankToNull(taskId);
stepId = blankToNull(stepId); stepId = Texts.blankToNull(stepId);
actor = blankToNull(actor); actor = Texts.blankToNull(actor);
detail = blankToNull(detail); detail = Texts.blankToNull(detail);
}
private static String blankToNull(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
} }
} }
@@ -26,14 +26,14 @@ public sealed interface RoutingPredicate permits RoutingPredicate.Compare, Routi
record Compare(Op op, String var, Object value) implements RoutingPredicate { record Compare(Op op, String var, Object value) implements RoutingPredicate {
public Compare { public Compare {
Objects.requireNonNull(op, "op must not be null"); Objects.requireNonNull(op, "op must not be null");
ApprovalStep.requireText(var, "var"); Texts.requireText(var, "var");
value = requireLiteral(value, "value"); value = requireLiteral(value, "value");
} }
} }
record In(String var, List<Object> values) implements RoutingPredicate { record In(String var, List<Object> values) implements RoutingPredicate {
public In { public In {
ApprovalStep.requireText(var, "var"); Texts.requireText(var, "var");
Objects.requireNonNull(values, "values must not be null"); Objects.requireNonNull(values, "values must not be null");
if (values.isEmpty()) { if (values.isEmpty()) {
throw new IllegalArgumentException("in values must not be empty"); throw new IllegalArgumentException("in values must not be empty");
@@ -15,7 +15,7 @@ import java.util.Objects;
* {@link RoutingCondition} reference with arguments. * {@link RoutingCondition} reference with arguments.
*/ */
public sealed interface RoutingWhen permits RoutingWhen.Predicate, RoutingWhen.Ref { public sealed interface RoutingWhen permits RoutingWhen.Predicate, RoutingWhen.Ref {
ObjectMapper MAPPER = new ObjectMapper(); ObjectMapper MAPPER = Jsons.MAPPER;
record Predicate(RoutingPredicate tree) implements RoutingWhen { record Predicate(RoutingPredicate tree) implements RoutingWhen {
public Predicate { public Predicate {
@@ -25,7 +25,7 @@ public sealed interface RoutingWhen permits RoutingWhen.Predicate, RoutingWhen.R
record Ref(String key, Map<String, Object> args) implements RoutingWhen { record Ref(String key, Map<String, Object> args) implements RoutingWhen {
public Ref { public Ref {
ApprovalStep.requireText(key, "ref"); Texts.requireText(key, "ref");
args = Map.copyOf(args == null ? Map.of() : args); args = Map.copyOf(args == null ? Map.of() : args);
} }
} }
@@ -11,8 +11,8 @@ public record StepDue(Duration after, DueThen then, String to, String action) {
throw new IllegalArgumentException("due after must be positive"); throw new IllegalArgumentException("due after must be positive");
} }
Objects.requireNonNull(then, "due then must not be null"); Objects.requireNonNull(then, "due then must not be null");
to = blankToNull(to); to = Texts.blankToNull(to);
action = blankToNull(action); action = Texts.blankToNull(action);
switch (then) { switch (then) {
case REASSIGN -> { case REASSIGN -> {
if (to == null) { if (to == null) {
@@ -50,7 +50,4 @@ public record StepDue(Duration after, DueThen then, String to, String action) {
return new StepDue(after, DueThen.GOTO, stepId, null); return new StepDue(after, DueThen.GOTO, stepId, null);
} }
private static String blankToNull(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
} }
@@ -7,7 +7,7 @@ package com.jetlumen.ordo.api;
*/ */
public record StepTransition(String fromStepId, String toStepId, RoutingWhen when, int priority) { public record StepTransition(String fromStepId, String toStepId, RoutingWhen when, int priority) {
public StepTransition { public StepTransition {
ApprovalStep.requireText(fromStepId, "fromStepId"); Texts.requireText(fromStepId, "fromStepId");
if (toStepId != null && toStepId.isBlank()) { if (toStepId != null && toStepId.isBlank()) {
throw new IllegalArgumentException("toStepId must not be blank when present"); throw new IllegalArgumentException("toStepId must not be blank when present");
} }
@@ -6,10 +6,8 @@ import java.util.Objects;
/** Immutable audit record created when an approval task is completed. */ /** Immutable audit record created when an approval task is completed. */
public record TaskAction(String actor, String comment, Instant operatedAt) { public record TaskAction(String actor, String comment, Instant operatedAt) {
public TaskAction { public TaskAction {
if (actor == null || actor.isBlank()) { Texts.requireText(actor, "actor");
throw new IllegalArgumentException("actor must not be blank"); comment = Texts.blankToNull(comment);
}
comment = comment == null || comment.isBlank() ? null : comment.strip();
Objects.requireNonNull(operatedAt, "operatedAt must not be null"); Objects.requireNonNull(operatedAt, "operatedAt must not be null");
} }
} }
@@ -0,0 +1,17 @@
package com.jetlumen.ordo.api;
/** Shared blank-string checks used by domain records and the engine. */
public final class Texts {
private Texts() {
}
public static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
}
public static String blankToNull(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
}
@@ -53,6 +53,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import static com.jetlumen.ordo.api.Texts.requireText;
/** /**
* Repository-backed implementation of the approval runtime with explicit step transitions. * Repository-backed implementation of the approval runtime with explicit step transitions.
@@ -653,12 +654,6 @@ public final class DefaultOrdoEngine implements OrdoEngine {
return UUID.randomUUID().toString(); return UUID.randomUUID().toString();
} }
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
}
private record PendingAction(String executionId, String actionKey, String instanceId, String stepId, private record PendingAction(String executionId, String actionKey, String instanceId, String stepId,
ProcessContext context) { ProcessContext context) {
} }
@@ -2,14 +2,13 @@ package com.jetlumen.ordo.spring.rest;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.jetlumen.ordo.api.Jsons;
import com.jetlumen.ordo.api.ProcessDefinition; import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessDefinitionParser; import com.jetlumen.ordo.api.ProcessDefinitionParser;
import java.util.Map; import java.util.Map;
final class DefinitionJson { final class DefinitionJson {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final TypeReference<Map<String, Object>> MAP = new TypeReference<>() { private static final TypeReference<Map<String, Object>> MAP = new TypeReference<>() {
}; };
@@ -18,7 +17,7 @@ final class DefinitionJson {
static Map<String, Object> map(ProcessDefinition definition) { static Map<String, Object> map(ProcessDefinition definition) {
try { try {
return MAPPER.readValue(ProcessDefinitionParser.toJson(definition), MAP); return Jsons.MAPPER.readValue(ProcessDefinitionParser.toJson(definition), MAP);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
@@ -26,7 +25,7 @@ final class DefinitionJson {
static String write(Map<String, Object> body) { static String write(Map<String, Object> body) {
try { try {
return MAPPER.writeValueAsString(body); return Jsons.MAPPER.writeValueAsString(body);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalArgumentException("invalid process definition json", e); throw new IllegalArgumentException("invalid process definition json", e);
} }
@@ -10,6 +10,7 @@ import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects; import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalStepMapper; import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalStepMapper;
import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalStepMapper.StepRow; import com.jetlumen.ordo.storage.jdbc.mapper.ApprovalStepMapper.StepRow;
import com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings;
import com.jetlumen.ordo.storage.jdbc.mapper.StepTransitionMapper; import com.jetlumen.ordo.storage.jdbc.mapper.StepTransitionMapper;
import com.jetlumen.ordo.storage.jdbc.mapper.StepTransitionMapper.TransitionRow; import com.jetlumen.ordo.storage.jdbc.mapper.StepTransitionMapper.TransitionRow;
@@ -345,17 +346,17 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
insertStep.setString(5, step.policy().name()); insertStep.setString(5, step.policy().name());
insertStep.setInt(6, stepOrder++); insertStep.setInt(6, stepOrder++);
insertStep.setString(7, step.kind().name()); insertStep.setString(7, step.kind().name());
insertStep.setString(8, step.actionKey()); SqlBindings.setString(insertStep, 8, step.actionKey());
if (step.due() == null) { if (step.due() == null) {
insertStep.setString(9, null); SqlBindings.setString(insertStep, 9, null);
insertStep.setString(10, null); SqlBindings.setString(insertStep, 10, null);
insertStep.setString(11, null); SqlBindings.setString(insertStep, 11, null);
insertStep.setString(12, null); SqlBindings.setString(insertStep, 12, null);
} else { } else {
insertStep.setString(9, step.due().after().toString()); insertStep.setString(9, step.due().after().toString());
insertStep.setString(10, step.due().then().name()); insertStep.setString(10, step.due().then().name());
insertStep.setString(11, step.due().to()); SqlBindings.setString(insertStep, 11, step.due().to());
insertStep.setString(12, step.due().action()); SqlBindings.setString(insertStep, 12, step.due().action());
} }
insertStep.executeUpdate(); insertStep.executeUpdate();
} }
@@ -376,8 +377,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
insertTransition.setString(1, definition.id()); insertTransition.setString(1, definition.id());
insertTransition.setInt(2, definition.version()); insertTransition.setInt(2, definition.version());
insertTransition.setString(3, transition.fromStepId()); insertTransition.setString(3, transition.fromStepId());
insertTransition.setString(4, transition.toStepId()); SqlBindings.setString(insertTransition, 4, transition.toStepId());
insertTransition.setString(5, transition.when() == null ? null : transition.when().toJson()); SqlBindings.setString(insertTransition, 5, transition.when() == null ? null : transition.when().toJson());
insertTransition.setInt(6, transition.priority()); insertTransition.setInt(6, transition.priority());
insertTransition.executeUpdate(); insertTransition.executeUpdate();
} }
@@ -6,8 +6,11 @@ import com.jetlumen.ordo.api.ActionExecutionStatus;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp; import java.time.Instant;
import java.sql.Types;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.getInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setBlankAsNull;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setInstant;
/** Maps rows of {@code ordo_action_execution} to {@link ActionExecution} objects and back. */ /** Maps rows of {@code ordo_action_execution} to {@link ActionExecution} objects and back. */
public final class ActionExecutionMapper { public final class ActionExecutionMapper {
@@ -20,11 +23,10 @@ public final class ActionExecutionMapper {
statement.setString(3, execution.stepId()); statement.setString(3, execution.stepId());
statement.setString(4, execution.actionKey()); statement.setString(4, execution.actionKey());
statement.setString(5, execution.status().name()); statement.setString(5, execution.status().name());
statement.setTimestamp(6, Timestamp.from(execution.startedAt())); setInstant(statement, 6, execution.startedAt());
} }
public static ActionExecution read(ResultSet resultSet) throws SQLException { public static ActionExecution read(ResultSet resultSet) throws SQLException {
Timestamp finishedAt = resultSet.getTimestamp("finished_at");
return new ActionExecution( return new ActionExecution(
resultSet.getString("id"), resultSet.getString("id"),
resultSet.getString("instance_id"), resultSet.getString("instance_id"),
@@ -32,19 +34,15 @@ public final class ActionExecutionMapper {
resultSet.getString("action_key"), resultSet.getString("action_key"),
ActionExecutionStatus.valueOf(resultSet.getString("status")), ActionExecutionStatus.valueOf(resultSet.getString("status")),
resultSet.getString("error_message"), resultSet.getString("error_message"),
resultSet.getTimestamp("started_at").toInstant(), getInstant(resultSet, "started_at"),
finishedAt == null ? null : finishedAt.toInstant()); getInstant(resultSet, "finished_at"));
} }
public static void bindComplete(PreparedStatement statement, String executionId, ActionExecutionStatus status, public static void bindComplete(PreparedStatement statement, String executionId, ActionExecutionStatus status,
String errorMessage, java.time.Instant finishedAt) throws SQLException { String errorMessage, Instant finishedAt) throws SQLException {
statement.setString(1, status.name()); statement.setString(1, status.name());
if (errorMessage == null || errorMessage.isBlank()) { setBlankAsNull(statement, 2, errorMessage);
statement.setNull(2, Types.VARCHAR); setInstant(statement, 3, finishedAt);
} else {
statement.setString(2, errorMessage);
}
statement.setTimestamp(3, Timestamp.from(finishedAt));
statement.setString(4, executionId); statement.setString(4, executionId);
} }
} }
@@ -7,8 +7,10 @@ import com.jetlumen.ordo.api.TaskStatus;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types; import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.getInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setString;
/** Maps rows of {@code ordo_approval_task} to {@link ApprovalTask} objects and back. */ /** Maps rows of {@code ordo_approval_task} to {@link ApprovalTask} objects and back. */
public final class ApprovalTaskMapper { public final class ApprovalTaskMapper {
@@ -22,27 +24,23 @@ public final class ApprovalTaskMapper {
statement.setString(4, task.name()); statement.setString(4, task.name());
statement.setString(5, task.assignee()); statement.setString(5, task.assignee());
statement.setString(6, task.status().name()); statement.setString(6, task.status().name());
statement.setTimestamp(7, Timestamp.from(task.createdAt())); setInstant(statement, 7, task.createdAt());
if (task.dueAt() == null) { setInstant(statement, 8, task.dueAt());
statement.setNull(8, Types.TIMESTAMP);
} else {
statement.setTimestamp(8, Timestamp.from(task.dueAt()));
}
} }
public static void bindComplete(PreparedStatement statement, ApprovalTask completedTask) throws SQLException { public static void bindComplete(PreparedStatement statement, ApprovalTask completedTask) throws SQLException {
TaskAction action = completedTask.action(); TaskAction action = completedTask.action();
statement.setString(1, completedTask.status().name()); statement.setString(1, completedTask.status().name());
statement.setTimestamp(2, Timestamp.from(completedTask.completedAt())); setInstant(statement, 2, completedTask.completedAt());
if (action == null) { if (action == null) {
// SKIPPED tasks have no actor - a sibling candidate's decision already settled the step. // SKIPPED tasks have no actor - a sibling candidate's decision already settled the step.
statement.setNull(3, Types.VARCHAR); setString(statement, 3, null);
statement.setNull(4, Types.VARCHAR); setString(statement, 4, null);
statement.setNull(5, Types.TIMESTAMP); setInstant(statement, 5, null);
} else { } else {
statement.setString(3, action.actor()); statement.setString(3, action.actor());
statement.setString(4, action.comment()); setString(statement, 4, action.comment());
statement.setTimestamp(5, Timestamp.from(action.operatedAt())); setInstant(statement, 5, action.operatedAt());
} }
statement.setString(6, completedTask.id()); statement.setString(6, completedTask.id());
statement.setString(7, completedTask.assignee()); statement.setString(7, completedTask.assignee());
@@ -50,11 +48,8 @@ public final class ApprovalTaskMapper {
public static ApprovalTask read(ResultSet resultSet) throws SQLException { public static ApprovalTask read(ResultSet resultSet) throws SQLException {
String actionActor = resultSet.getString("action_actor"); String actionActor = resultSet.getString("action_actor");
Timestamp actionAt = resultSet.getTimestamp("action_at");
TaskAction action = actionActor == null ? null TaskAction action = actionActor == null ? null
: new TaskAction(actionActor, resultSet.getString("action_comment"), actionAt.toInstant()); : new TaskAction(actionActor, resultSet.getString("action_comment"), getInstant(resultSet, "action_at"));
Timestamp completedAt = resultSet.getTimestamp("completed_at");
Timestamp dueAt = resultSet.getTimestamp("due_at");
return new ApprovalTask( return new ApprovalTask(
resultSet.getString("id"), resultSet.getString("id"),
resultSet.getString("instance_id"), resultSet.getString("instance_id"),
@@ -62,9 +57,9 @@ public final class ApprovalTaskMapper {
resultSet.getString("task_name"), resultSet.getString("task_name"),
resultSet.getString("assignee"), resultSet.getString("assignee"),
TaskStatus.valueOf(resultSet.getString("status")), TaskStatus.valueOf(resultSet.getString("status")),
resultSet.getTimestamp("created_at").toInstant(), getInstant(resultSet, "created_at"),
completedAt == null ? null : completedAt.toInstant(), getInstant(resultSet, "completed_at"),
action, action,
dueAt == null ? null : dueAt.toInstant()); getInstant(resultSet, "due_at"));
} }
} }
@@ -2,7 +2,7 @@ package com.jetlumen.ordo.storage.jdbc.mapper;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.jetlumen.ordo.api.Jsons;
import com.jetlumen.ordo.api.ProcessContext; import com.jetlumen.ordo.api.ProcessContext;
import java.util.Map; import java.util.Map;
@@ -13,14 +13,12 @@ import java.util.Map;
* booleans, lists and nested maps. * booleans, lists and nested maps.
*/ */
public final class ProcessContextCodec { public final class ProcessContextCodec {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private ProcessContextCodec() { private ProcessContextCodec() {
} }
public static String encode(ProcessContext context) { public static String encode(ProcessContext context) {
try { try {
return OBJECT_MAPPER.writeValueAsString(context.variables()); return Jsons.MAPPER.writeValueAsString(context.variables());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalStateException("failed to serialize process context to JSON", e); throw new IllegalStateException("failed to serialize process context to JSON", e);
} }
@@ -28,7 +26,7 @@ public final class ProcessContextCodec {
public static ProcessContext decode(String json) { public static ProcessContext decode(String json) {
try { try {
Map<String, Object> variables = OBJECT_MAPPER.readValue(json, new TypeReference<>() { Map<String, Object> variables = Jsons.MAPPER.readValue(json, new TypeReference<>() {
}); });
return new ProcessContext(variables); return new ProcessContext(variables);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -6,8 +6,10 @@ import com.jetlumen.ordo.api.ProcessEventType;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types; import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.getInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setString;
/** Maps rows of {@code ordo_process_event} to {@link ProcessEvent} objects and back. */ /** Maps rows of {@code ordo_process_event} to {@link ProcessEvent} objects and back. */
public final class ProcessEventMapper { public final class ProcessEventMapper {
@@ -17,12 +19,12 @@ public final class ProcessEventMapper {
public static void bindInsert(PreparedStatement statement, ProcessEvent event) throws SQLException { public static void bindInsert(PreparedStatement statement, ProcessEvent event) throws SQLException {
statement.setString(1, event.id()); statement.setString(1, event.id());
statement.setString(2, event.instanceId()); statement.setString(2, event.instanceId());
setNullableString(statement, 3, event.taskId()); setString(statement, 3, event.taskId());
setNullableString(statement, 4, event.stepId()); setString(statement, 4, event.stepId());
statement.setString(5, event.type().name()); statement.setString(5, event.type().name());
setNullableString(statement, 6, event.actor()); setString(statement, 6, event.actor());
setNullableString(statement, 7, event.detail()); setString(statement, 7, event.detail());
statement.setTimestamp(8, Timestamp.from(event.occurredAt())); setInstant(statement, 8, event.occurredAt());
} }
public static ProcessEvent read(ResultSet resultSet) throws SQLException { public static ProcessEvent read(ResultSet resultSet) throws SQLException {
@@ -34,14 +36,6 @@ public final class ProcessEventMapper {
ProcessEventType.valueOf(resultSet.getString("event_type")), ProcessEventType.valueOf(resultSet.getString("event_type")),
resultSet.getString("actor"), resultSet.getString("actor"),
resultSet.getString("detail"), resultSet.getString("detail"),
resultSet.getTimestamp("occurred_at").toInstant()); getInstant(resultSet, "occurred_at"));
}
private static void setNullableString(PreparedStatement statement, int index, String value) throws SQLException {
if (value == null) {
statement.setNull(index, Types.VARCHAR);
} else {
statement.setString(index, value);
}
} }
} }
@@ -6,7 +6,9 @@ import com.jetlumen.ordo.api.ProcessStatus;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.getInstant;
import static com.jetlumen.ordo.storage.jdbc.mapper.SqlBindings.setInstant;
/** Maps rows of {@code ordo_process_instance} to {@link ProcessInstance} objects and back. */ /** Maps rows of {@code ordo_process_instance} to {@link ProcessInstance} objects and back. */
public final class ProcessInstanceMapper { public final class ProcessInstanceMapper {
@@ -20,27 +22,25 @@ public final class ProcessInstanceMapper {
statement.setString(4, instance.initiator()); statement.setString(4, instance.initiator());
statement.setString(5, instance.status().name()); statement.setString(5, instance.status().name());
statement.setString(6, ProcessContextCodec.encode(instance.context())); statement.setString(6, ProcessContextCodec.encode(instance.context()));
statement.setTimestamp(7, Timestamp.from(instance.startedAt())); setInstant(statement, 7, instance.startedAt());
statement.setTimestamp(8, instance.finishedAt() == null ? null : Timestamp.from(instance.finishedAt())); setInstant(statement, 8, instance.finishedAt());
} }
public static void bindUpdate(PreparedStatement statement, ProcessInstance instance) throws SQLException { public static void bindUpdate(PreparedStatement statement, ProcessInstance instance) throws SQLException {
statement.setString(1, instance.status().name()); statement.setString(1, instance.status().name());
statement.setTimestamp(2, instance.finishedAt() == null ? null : Timestamp.from(instance.finishedAt())); setInstant(statement, 2, instance.finishedAt());
statement.setString(3, instance.id()); statement.setString(3, instance.id());
} }
public static ProcessInstance read(ResultSet resultSet) throws SQLException { public static ProcessInstance read(ResultSet resultSet) throws SQLException {
Timestamp startedAt = resultSet.getTimestamp("started_at");
Timestamp finishedAt = resultSet.getTimestamp("finished_at");
return new ProcessInstance( return new ProcessInstance(
resultSet.getString("id"), resultSet.getString("id"),
resultSet.getString("definition_id"), resultSet.getString("definition_id"),
resultSet.getInt("definition_version"), resultSet.getInt("definition_version"),
resultSet.getString("initiator"), resultSet.getString("initiator"),
ProcessStatus.valueOf(resultSet.getString("status")), ProcessStatus.valueOf(resultSet.getString("status")),
startedAt.toInstant(), getInstant(resultSet, "started_at"),
finishedAt == null ? null : finishedAt.toInstant(), getInstant(resultSet, "finished_at"),
ProcessContextCodec.decode(resultSet.getString("context_json"))); ProcessContextCodec.decode(resultSet.getString("context_json")));
} }
} }
@@ -0,0 +1,43 @@
package com.jetlumen.ordo.storage.jdbc.mapper;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
/** Nullable JDBC parameter and column helpers. */
public final class SqlBindings {
private SqlBindings() {
}
public static void setString(PreparedStatement statement, int index, String value) throws SQLException {
if (value == null) {
statement.setNull(index, Types.VARCHAR);
} else {
statement.setString(index, value);
}
}
public static void setBlankAsNull(PreparedStatement statement, int index, String value) throws SQLException {
if (value == null || value.isBlank()) {
statement.setNull(index, Types.VARCHAR);
} else {
statement.setString(index, value);
}
}
public static void setInstant(PreparedStatement statement, int index, Instant value) throws SQLException {
if (value == null) {
statement.setNull(index, Types.TIMESTAMP);
} else {
statement.setTimestamp(index, Timestamp.from(value));
}
}
public static Instant getInstant(ResultSet resultSet, String column) throws SQLException {
Timestamp timestamp = resultSet.getTimestamp(column);
return timestamp == null ? null : timestamp.toInstant();
}
}
@@ -66,8 +66,8 @@ class JdbcMysqlIntegrationTest {
reachabilityProblem = e.getMessage(); reachabilityProblem = e.getMessage();
} }
Assumptions.assumeTrue(reachabilityProblem == null, Assumptions.assumeTrue(reachabilityProblem == null,
"MySQL not reachable at " + config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost") "MySQL not reachable at " + JdbcTestSupport.config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost")
+ ":" + config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306") + ":" + JdbcTestSupport.config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306")
+ " — skipping MySQL integration tests (" + reachabilityProblem + ")"); + " — skipping MySQL integration tests (" + reachabilityProblem + ")");
try (Connection connection = rootDataSource.getConnection(); Statement statement = connection.createStatement()) { try (Connection connection = rootDataSource.getConnection(); Statement statement = connection.createStatement()) {
@@ -238,28 +238,16 @@ class JdbcMysqlIntegrationTest {
private static MysqlDataSource newDataSource(String database) { private static MysqlDataSource newDataSource(String database) {
MysqlDataSource mysqlDataSource = new MysqlDataSource(); MysqlDataSource mysqlDataSource = new MysqlDataSource();
String host = config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost"); String host = JdbcTestSupport.config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost");
String port = config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306"); String port = JdbcTestSupport.config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306");
String databasePath = database.isEmpty() ? "/" : "/" + database; String databasePath = database.isEmpty() ? "/" : "/" + database;
mysqlDataSource.setUrl("jdbc:mysql://" + host + ":" + port + databasePath mysqlDataSource.setUrl("jdbc:mysql://" + host + ":" + port + databasePath
+ "?allowPublicKeyRetrieval=true&sslMode=DISABLED&characterEncoding=utf8"); + "?allowPublicKeyRetrieval=true&sslMode=DISABLED&characterEncoding=utf8");
mysqlDataSource.setUser(config("ordo.test.mysql.user", "ORDO_TEST_MYSQL_USER", "root")); mysqlDataSource.setUser(JdbcTestSupport.config("ordo.test.mysql.user", "ORDO_TEST_MYSQL_USER", "root"));
mysqlDataSource.setPassword(config("ordo.test.mysql.password", "ORDO_TEST_MYSQL_PASSWORD", "root")); mysqlDataSource.setPassword(JdbcTestSupport.config("ordo.test.mysql.password", "ORDO_TEST_MYSQL_PASSWORD", "root"));
return mysqlDataSource; return mysqlDataSource;
} }
private static String config(String property, String env, String defaultValue) {
String fromProperty = System.getProperty(property);
if (fromProperty != null && !fromProperty.isBlank()) {
return fromProperty;
}
String fromEnv = System.getenv(env);
if (fromEnv != null && !fromEnv.isBlank()) {
return fromEnv;
}
return defaultValue;
}
private static void attempt(CountDownLatch start, CountDownLatch done, AtomicInteger wins, private static void attempt(CountDownLatch start, CountDownLatch done, AtomicInteger wins,
ApprovalTask completed, JdbcApprovalTaskRepository repository) { ApprovalTask completed, JdbcApprovalTaskRepository repository) {
try { try {
@@ -234,29 +234,17 @@ class JdbcPostgresIntegrationTest {
private static PGSimpleDataSource newDataSource(String currentSchema) { private static PGSimpleDataSource newDataSource(String currentSchema) {
PGSimpleDataSource pgDataSource = new PGSimpleDataSource(); PGSimpleDataSource pgDataSource = new PGSimpleDataSource();
pgDataSource.setServerNames(new String[]{config("ordo.test.pg.host", "ORDO_TEST_PG_HOST", "localhost")}); pgDataSource.setServerNames(new String[]{JdbcTestSupport.config("ordo.test.pg.host", "ORDO_TEST_PG_HOST", "localhost")});
pgDataSource.setPortNumbers(new int[]{Integer.parseInt(config("ordo.test.pg.port", "ORDO_TEST_PG_PORT", "5432"))}); pgDataSource.setPortNumbers(new int[]{Integer.parseInt(JdbcTestSupport.config("ordo.test.pg.port", "ORDO_TEST_PG_PORT", "5432"))});
pgDataSource.setDatabaseName(config("ordo.test.pg.database", "ORDO_TEST_PG_DATABASE", "postgres")); pgDataSource.setDatabaseName(JdbcTestSupport.config("ordo.test.pg.database", "ORDO_TEST_PG_DATABASE", "postgres"));
pgDataSource.setUser(config("ordo.test.pg.user", "ORDO_TEST_PG_USER", "postgres")); pgDataSource.setUser(JdbcTestSupport.config("ordo.test.pg.user", "ORDO_TEST_PG_USER", "postgres"));
pgDataSource.setPassword(config("ordo.test.pg.password", "ORDO_TEST_PG_PASSWORD", "postgres")); pgDataSource.setPassword(JdbcTestSupport.config("ordo.test.pg.password", "ORDO_TEST_PG_PASSWORD", "postgres"));
if (!currentSchema.isEmpty()) { if (!currentSchema.isEmpty()) {
pgDataSource.setCurrentSchema(currentSchema); pgDataSource.setCurrentSchema(currentSchema);
} }
return pgDataSource; return pgDataSource;
} }
private static String config(String property, String env, String defaultValue) {
String fromProperty = System.getProperty(property);
if (fromProperty != null && !fromProperty.isBlank()) {
return fromProperty;
}
String fromEnv = System.getenv(env);
if (fromEnv != null && !fromEnv.isBlank()) {
return fromEnv;
}
return defaultValue;
}
private static void attempt(CountDownLatch start, CountDownLatch done, AtomicInteger wins, private static void attempt(CountDownLatch start, CountDownLatch done, AtomicInteger wins,
ApprovalTask completed, JdbcApprovalTaskRepository repository) { ApprovalTask completed, JdbcApprovalTaskRepository repository) {
try { try {
@@ -30,6 +30,18 @@ final class JdbcTestSupport {
return dataSource; return dataSource;
} }
static String config(String property, String env, String defaultValue) {
String fromProperty = System.getProperty(property);
if (fromProperty != null && !fromProperty.isBlank()) {
return fromProperty;
}
String fromEnv = System.getenv(env);
if (fromEnv != null && !fromEnv.isBlank()) {
return fromEnv;
}
return defaultValue;
}
static void applySchema(DataSource dataSource) { static void applySchema(DataSource dataSource) {
applySchema(dataSource, POSTGRES_BASELINE, POSTGRES_V2); applySchema(dataSource, POSTGRES_BASELINE, POSTGRES_V2);
} }