diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ActionExecution.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ActionExecution.java index 745df5a..5a2e22d 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ActionExecution.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ActionExecution.java @@ -8,18 +8,12 @@ public record ActionExecution(String id, String instanceId, String stepId, Strin ActionExecutionStatus status, String errorMessage, Instant startedAt, Instant finishedAt) { public ActionExecution { - requireText(id, "id"); - requireText(instanceId, "instanceId"); - requireText(stepId, "stepId"); - requireText(actionKey, "actionKey"); + Texts.requireText(id, "id"); + Texts.requireText(instanceId, "instanceId"); + Texts.requireText(stepId, "stepId"); + Texts.requireText(actionKey, "actionKey"); Objects.requireNonNull(status, "status must not be null"); Objects.requireNonNull(startedAt, "startedAt must not be null"); - errorMessage = errorMessage == null || errorMessage.isBlank() ? null : errorMessage.strip(); - } - - private static void requireText(String value, String name) { - if (value == null || value.isBlank()) { - throw new IllegalArgumentException(name + " must not be blank"); - } + errorMessage = Texts.blankToNull(errorMessage); } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ApprovalStep.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ApprovalStep.java index 36bd3df..3767d7f 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ApprovalStep.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ApprovalStep.java @@ -12,8 +12,8 @@ import java.util.Set; public record ApprovalStep(String id, String name, List candidates, ApprovalPolicy policy, StepKind kind, String actionKey, StepDue due) { public ApprovalStep { - requireText(id, "step id"); - requireText(name, "step name"); + Texts.requireText(id, "step id"); + Texts.requireText(name, "step name"); kind = kind == null ? StepKind.APPROVAL : kind; Objects.requireNonNull(policy, "policy must not be null"); Objects.requireNonNull(candidates, "candidates must not be null"); @@ -22,7 +22,7 @@ public record ApprovalStep(String id, String name, List candidates, Appr if (!candidates.isEmpty()) { throw new IllegalArgumentException("an action step must not have candidates"); } - requireText(actionKey, "action key"); + Texts.requireText(actionKey, "action key"); actionKey = actionKey.strip(); if (due != null) { throw new IllegalArgumentException("an action step must not have due"); @@ -37,7 +37,7 @@ public record ApprovalStep(String id, String name, List candidates, Appr } Set distinct = new HashSet<>(); for (String candidate : candidates) { - requireText(candidate, "candidate"); + Texts.requireText(candidate, "candidate"); if (!distinct.add(candidate)) { throw new IllegalArgumentException("duplicate candidate in step " + id + ": " + candidate); } @@ -62,10 +62,4 @@ public record ApprovalStep(String id, String name, List candidates, Appr public static ApprovalStep action(String id, String name, String 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"); - } - } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/Jsons.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/Jsons.java new file mode 100644 index 0000000..1d1462f --- /dev/null +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/Jsons.java @@ -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() { + } +} diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoCatalog.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoCatalog.java index c77d123..933cfa5 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoCatalog.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoCatalog.java @@ -29,9 +29,9 @@ public interface OrdoCatalog { record Param(String name, String type, String label) { public Param { - ApprovalStep.requireText(name, "param name"); - ApprovalStep.requireText(type, "param type"); - ApprovalStep.requireText(label, "param label"); + Texts.requireText(name, "param name"); + Texts.requireText(type, "param type"); + Texts.requireText(label, "param label"); type = type.strip().toLowerCase(Locale.ROOT); if (!PARAM_TYPES.contains(type)) { throw new IllegalArgumentException("param type must be number, string or boolean"); diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java index a652aa2..61cff8a 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java @@ -11,11 +11,11 @@ import java.util.Set; public record ProcessDefinition(String id, int version, String name, List steps, List transitions) { public ProcessDefinition { - ApprovalStep.requireText(id, "definition id"); + Texts.requireText(id, "definition id"); if (version < 0) { throw new IllegalArgumentException("definition version must not be negative"); } - ApprovalStep.requireText(name, "definition name"); + Texts.requireText(name, "definition name"); steps = List.copyOf(steps); if (steps.isEmpty()) { throw new IllegalArgumentException("a definition must contain at least one step"); diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinitionParser.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinitionParser.java index e496480..146e790 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinitionParser.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinitionParser.java @@ -1,6 +1,5 @@ package com.jetlumen.ordo.api; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -17,8 +16,7 @@ import java.util.Objects; /** Parses a structural JSON process graph into a {@link ProcessDefinition}. */ public final class ProcessDefinitionParser { - private static final ObjectMapper MAPPER = new ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL); + private static final ObjectMapper MAPPER = Jsons.NON_NULL; private ProcessDefinitionParser() { } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessEvent.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessEvent.java index 9ac5f4e..f90abe0 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessEvent.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessEvent.java @@ -7,23 +7,13 @@ import java.util.Objects; public record ProcessEvent(String id, String instanceId, String taskId, String stepId, ProcessEventType type, String actor, String detail, Instant occurredAt) { public ProcessEvent { - requireText(id, "id"); - requireText(instanceId, "instanceId"); + Texts.requireText(id, "id"); + Texts.requireText(instanceId, "instanceId"); Objects.requireNonNull(type, "type must not be null"); Objects.requireNonNull(occurredAt, "occurredAt must not be null"); - taskId = blankToNull(taskId); - stepId = blankToNull(stepId); - actor = blankToNull(actor); - detail = 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"); - } + taskId = Texts.blankToNull(taskId); + stepId = Texts.blankToNull(stepId); + actor = Texts.blankToNull(actor); + detail = Texts.blankToNull(detail); } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingPredicate.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingPredicate.java index d4760ce..8d974ab 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingPredicate.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingPredicate.java @@ -26,14 +26,14 @@ public sealed interface RoutingPredicate permits RoutingPredicate.Compare, Routi record Compare(Op op, String var, Object value) implements RoutingPredicate { public Compare { Objects.requireNonNull(op, "op must not be null"); - ApprovalStep.requireText(var, "var"); + Texts.requireText(var, "var"); value = requireLiteral(value, "value"); } } record In(String var, List values) implements RoutingPredicate { public In { - ApprovalStep.requireText(var, "var"); + Texts.requireText(var, "var"); Objects.requireNonNull(values, "values must not be null"); if (values.isEmpty()) { throw new IllegalArgumentException("in values must not be empty"); diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingWhen.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingWhen.java index f50b743..9b12aca 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingWhen.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/RoutingWhen.java @@ -15,7 +15,7 @@ import java.util.Objects; * {@link RoutingCondition} reference with arguments. */ public sealed interface RoutingWhen permits RoutingWhen.Predicate, RoutingWhen.Ref { - ObjectMapper MAPPER = new ObjectMapper(); + ObjectMapper MAPPER = Jsons.MAPPER; record Predicate(RoutingPredicate tree) implements RoutingWhen { public Predicate { @@ -25,7 +25,7 @@ public sealed interface RoutingWhen permits RoutingWhen.Predicate, RoutingWhen.R record Ref(String key, Map args) implements RoutingWhen { public Ref { - ApprovalStep.requireText(key, "ref"); + Texts.requireText(key, "ref"); args = Map.copyOf(args == null ? Map.of() : args); } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/StepDue.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/StepDue.java index 15c98b7..65e2c40 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/StepDue.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/StepDue.java @@ -11,8 +11,8 @@ public record StepDue(Duration after, DueThen then, String to, String action) { throw new IllegalArgumentException("due after must be positive"); } Objects.requireNonNull(then, "due then must not be null"); - to = blankToNull(to); - action = blankToNull(action); + to = Texts.blankToNull(to); + action = Texts.blankToNull(action); switch (then) { case REASSIGN -> { 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); } - private static String blankToNull(String value) { - return value == null || value.isBlank() ? null : value.strip(); - } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/StepTransition.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/StepTransition.java index d2a2956..974df18 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/StepTransition.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/StepTransition.java @@ -7,7 +7,7 @@ package com.jetlumen.ordo.api; */ public record StepTransition(String fromStepId, String toStepId, RoutingWhen when, int priority) { public StepTransition { - ApprovalStep.requireText(fromStepId, "fromStepId"); + Texts.requireText(fromStepId, "fromStepId"); if (toStepId != null && toStepId.isBlank()) { throw new IllegalArgumentException("toStepId must not be blank when present"); } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/TaskAction.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/TaskAction.java index f5d12c9..79f71ed 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/TaskAction.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/TaskAction.java @@ -6,10 +6,8 @@ import java.util.Objects; /** Immutable audit record created when an approval task is completed. */ public record TaskAction(String actor, String comment, Instant operatedAt) { public TaskAction { - if (actor == null || actor.isBlank()) { - throw new IllegalArgumentException("actor must not be blank"); - } - comment = comment == null || comment.isBlank() ? null : comment.strip(); + Texts.requireText(actor, "actor"); + comment = Texts.blankToNull(comment); Objects.requireNonNull(operatedAt, "operatedAt must not be null"); } } diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/Texts.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/Texts.java new file mode 100644 index 0000000..fa69011 --- /dev/null +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/Texts.java @@ -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(); + } +} diff --git a/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java b/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java index a157a8f..3af10f2 100644 --- a/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java +++ b/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java @@ -53,6 +53,7 @@ import java.util.Optional; import java.util.UUID; 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. @@ -653,12 +654,6 @@ public final class DefaultOrdoEngine implements OrdoEngine { 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, ProcessContext context) { } diff --git a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/rest/DefinitionJson.java b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/rest/DefinitionJson.java index f57b989..b4d2108 100644 --- a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/rest/DefinitionJson.java +++ b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/rest/DefinitionJson.java @@ -2,14 +2,13 @@ package com.jetlumen.ordo.spring.rest; import com.fasterxml.jackson.core.JsonProcessingException; 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.ProcessDefinitionParser; import java.util.Map; final class DefinitionJson { - private static final ObjectMapper MAPPER = new ObjectMapper(); private static final TypeReference> MAP = new TypeReference<>() { }; @@ -18,7 +17,7 @@ final class DefinitionJson { static Map map(ProcessDefinition definition) { try { - return MAPPER.readValue(ProcessDefinitionParser.toJson(definition), MAP); + return Jsons.MAPPER.readValue(ProcessDefinitionParser.toJson(definition), MAP); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } @@ -26,7 +25,7 @@ final class DefinitionJson { static String write(Map body) { try { - return MAPPER.writeValueAsString(body); + return Jsons.MAPPER.writeValueAsString(body); } catch (JsonProcessingException e) { throw new IllegalArgumentException("invalid process definition json", e); } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessDefinitionRepository.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessDefinitionRepository.java index cbfd2cf..b98aa8c 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessDefinitionRepository.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessDefinitionRepository.java @@ -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.mapper.ApprovalStepMapper; 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.TransitionRow; @@ -345,17 +346,17 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR insertStep.setString(5, step.policy().name()); insertStep.setInt(6, stepOrder++); insertStep.setString(7, step.kind().name()); - insertStep.setString(8, step.actionKey()); + SqlBindings.setString(insertStep, 8, step.actionKey()); if (step.due() == null) { - insertStep.setString(9, null); - insertStep.setString(10, null); - insertStep.setString(11, null); - insertStep.setString(12, null); + SqlBindings.setString(insertStep, 9, null); + SqlBindings.setString(insertStep, 10, null); + SqlBindings.setString(insertStep, 11, null); + SqlBindings.setString(insertStep, 12, null); } else { insertStep.setString(9, step.due().after().toString()); insertStep.setString(10, step.due().then().name()); - insertStep.setString(11, step.due().to()); - insertStep.setString(12, step.due().action()); + SqlBindings.setString(insertStep, 11, step.due().to()); + SqlBindings.setString(insertStep, 12, step.due().action()); } insertStep.executeUpdate(); } @@ -376,8 +377,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR insertTransition.setString(1, definition.id()); insertTransition.setInt(2, definition.version()); insertTransition.setString(3, transition.fromStepId()); - insertTransition.setString(4, transition.toStepId()); - insertTransition.setString(5, transition.when() == null ? null : transition.when().toJson()); + SqlBindings.setString(insertTransition, 4, transition.toStepId()); + SqlBindings.setString(insertTransition, 5, transition.when() == null ? null : transition.when().toJson()); insertTransition.setInt(6, transition.priority()); insertTransition.executeUpdate(); } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ActionExecutionMapper.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ActionExecutionMapper.java index 5dc2dbe..f657306 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ActionExecutionMapper.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ActionExecutionMapper.java @@ -6,8 +6,11 @@ import com.jetlumen.ordo.api.ActionExecutionStatus; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; -import java.sql.Types; +import java.time.Instant; + +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. */ public final class ActionExecutionMapper { @@ -20,11 +23,10 @@ public final class ActionExecutionMapper { statement.setString(3, execution.stepId()); statement.setString(4, execution.actionKey()); 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 { - Timestamp finishedAt = resultSet.getTimestamp("finished_at"); return new ActionExecution( resultSet.getString("id"), resultSet.getString("instance_id"), @@ -32,19 +34,15 @@ public final class ActionExecutionMapper { resultSet.getString("action_key"), ActionExecutionStatus.valueOf(resultSet.getString("status")), resultSet.getString("error_message"), - resultSet.getTimestamp("started_at").toInstant(), - finishedAt == null ? null : finishedAt.toInstant()); + getInstant(resultSet, "started_at"), + getInstant(resultSet, "finished_at")); } 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()); - if (errorMessage == null || errorMessage.isBlank()) { - statement.setNull(2, Types.VARCHAR); - } else { - statement.setString(2, errorMessage); - } - statement.setTimestamp(3, Timestamp.from(finishedAt)); + setBlankAsNull(statement, 2, errorMessage); + setInstant(statement, 3, finishedAt); statement.setString(4, executionId); } } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ApprovalTaskMapper.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ApprovalTaskMapper.java index d7593bb..865e0f6 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ApprovalTaskMapper.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ApprovalTaskMapper.java @@ -7,8 +7,10 @@ import com.jetlumen.ordo.api.TaskStatus; import java.sql.PreparedStatement; import java.sql.ResultSet; 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. */ public final class ApprovalTaskMapper { @@ -22,27 +24,23 @@ public final class ApprovalTaskMapper { statement.setString(4, task.name()); statement.setString(5, task.assignee()); statement.setString(6, task.status().name()); - statement.setTimestamp(7, Timestamp.from(task.createdAt())); - if (task.dueAt() == null) { - statement.setNull(8, Types.TIMESTAMP); - } else { - statement.setTimestamp(8, Timestamp.from(task.dueAt())); - } + setInstant(statement, 7, task.createdAt()); + setInstant(statement, 8, task.dueAt()); } public static void bindComplete(PreparedStatement statement, ApprovalTask completedTask) throws SQLException { TaskAction action = completedTask.action(); statement.setString(1, completedTask.status().name()); - statement.setTimestamp(2, Timestamp.from(completedTask.completedAt())); + setInstant(statement, 2, completedTask.completedAt()); if (action == null) { // SKIPPED tasks have no actor - a sibling candidate's decision already settled the step. - statement.setNull(3, Types.VARCHAR); - statement.setNull(4, Types.VARCHAR); - statement.setNull(5, Types.TIMESTAMP); + setString(statement, 3, null); + setString(statement, 4, null); + setInstant(statement, 5, null); } else { statement.setString(3, action.actor()); - statement.setString(4, action.comment()); - statement.setTimestamp(5, Timestamp.from(action.operatedAt())); + setString(statement, 4, action.comment()); + setInstant(statement, 5, action.operatedAt()); } statement.setString(6, completedTask.id()); statement.setString(7, completedTask.assignee()); @@ -50,11 +48,8 @@ public final class ApprovalTaskMapper { public static ApprovalTask read(ResultSet resultSet) throws SQLException { String actionActor = resultSet.getString("action_actor"); - Timestamp actionAt = resultSet.getTimestamp("action_at"); TaskAction action = actionActor == null ? null - : new TaskAction(actionActor, resultSet.getString("action_comment"), actionAt.toInstant()); - Timestamp completedAt = resultSet.getTimestamp("completed_at"); - Timestamp dueAt = resultSet.getTimestamp("due_at"); + : new TaskAction(actionActor, resultSet.getString("action_comment"), getInstant(resultSet, "action_at")); return new ApprovalTask( resultSet.getString("id"), resultSet.getString("instance_id"), @@ -62,9 +57,9 @@ public final class ApprovalTaskMapper { resultSet.getString("task_name"), resultSet.getString("assignee"), TaskStatus.valueOf(resultSet.getString("status")), - resultSet.getTimestamp("created_at").toInstant(), - completedAt == null ? null : completedAt.toInstant(), + getInstant(resultSet, "created_at"), + getInstant(resultSet, "completed_at"), action, - dueAt == null ? null : dueAt.toInstant()); + getInstant(resultSet, "due_at")); } } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessContextCodec.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessContextCodec.java index 000c602..c6f0665 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessContextCodec.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessContextCodec.java @@ -2,7 +2,7 @@ package com.jetlumen.ordo.storage.jdbc.mapper; import com.fasterxml.jackson.core.JsonProcessingException; 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 java.util.Map; @@ -13,14 +13,12 @@ import java.util.Map; * booleans, lists and nested maps. */ public final class ProcessContextCodec { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private ProcessContextCodec() { } public static String encode(ProcessContext context) { try { - return OBJECT_MAPPER.writeValueAsString(context.variables()); + return Jsons.MAPPER.writeValueAsString(context.variables()); } catch (JsonProcessingException 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) { try { - Map variables = OBJECT_MAPPER.readValue(json, new TypeReference<>() { + Map variables = Jsons.MAPPER.readValue(json, new TypeReference<>() { }); return new ProcessContext(variables); } catch (JsonProcessingException e) { diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessEventMapper.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessEventMapper.java index ef330c0..985f1d7 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessEventMapper.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessEventMapper.java @@ -6,8 +6,10 @@ import com.jetlumen.ordo.api.ProcessEventType; import java.sql.PreparedStatement; import java.sql.ResultSet; 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. */ public final class ProcessEventMapper { @@ -17,12 +19,12 @@ public final class ProcessEventMapper { public static void bindInsert(PreparedStatement statement, ProcessEvent event) throws SQLException { statement.setString(1, event.id()); statement.setString(2, event.instanceId()); - setNullableString(statement, 3, event.taskId()); - setNullableString(statement, 4, event.stepId()); + setString(statement, 3, event.taskId()); + setString(statement, 4, event.stepId()); statement.setString(5, event.type().name()); - setNullableString(statement, 6, event.actor()); - setNullableString(statement, 7, event.detail()); - statement.setTimestamp(8, Timestamp.from(event.occurredAt())); + setString(statement, 6, event.actor()); + setString(statement, 7, event.detail()); + setInstant(statement, 8, event.occurredAt()); } public static ProcessEvent read(ResultSet resultSet) throws SQLException { @@ -34,14 +36,6 @@ public final class ProcessEventMapper { ProcessEventType.valueOf(resultSet.getString("event_type")), resultSet.getString("actor"), resultSet.getString("detail"), - resultSet.getTimestamp("occurred_at").toInstant()); - } - - 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); - } + getInstant(resultSet, "occurred_at")); } } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessInstanceMapper.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessInstanceMapper.java index ff3f7bd..9457bc8 100644 --- a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessInstanceMapper.java +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/ProcessInstanceMapper.java @@ -6,7 +6,9 @@ import com.jetlumen.ordo.api.ProcessStatus; import java.sql.PreparedStatement; import java.sql.ResultSet; 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. */ public final class ProcessInstanceMapper { @@ -20,27 +22,25 @@ public final class ProcessInstanceMapper { statement.setString(4, instance.initiator()); statement.setString(5, instance.status().name()); statement.setString(6, ProcessContextCodec.encode(instance.context())); - statement.setTimestamp(7, Timestamp.from(instance.startedAt())); - statement.setTimestamp(8, instance.finishedAt() == null ? null : Timestamp.from(instance.finishedAt())); + setInstant(statement, 7, instance.startedAt()); + setInstant(statement, 8, instance.finishedAt()); } public static void bindUpdate(PreparedStatement statement, ProcessInstance instance) throws SQLException { 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()); } public static ProcessInstance read(ResultSet resultSet) throws SQLException { - Timestamp startedAt = resultSet.getTimestamp("started_at"); - Timestamp finishedAt = resultSet.getTimestamp("finished_at"); return new ProcessInstance( resultSet.getString("id"), resultSet.getString("definition_id"), resultSet.getInt("definition_version"), resultSet.getString("initiator"), ProcessStatus.valueOf(resultSet.getString("status")), - startedAt.toInstant(), - finishedAt == null ? null : finishedAt.toInstant(), + getInstant(resultSet, "started_at"), + getInstant(resultSet, "finished_at"), ProcessContextCodec.decode(resultSet.getString("context_json"))); } } diff --git a/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/SqlBindings.java b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/SqlBindings.java new file mode 100644 index 0000000..bd5c316 --- /dev/null +++ b/ordo-storage-jdbc/src/main/java/com/jetlumen/ordo/storage/jdbc/mapper/SqlBindings.java @@ -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(); + } +} diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcMysqlIntegrationTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcMysqlIntegrationTest.java index 3fd75a5..daf61d1 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcMysqlIntegrationTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcMysqlIntegrationTest.java @@ -66,8 +66,8 @@ class JdbcMysqlIntegrationTest { reachabilityProblem = e.getMessage(); } Assumptions.assumeTrue(reachabilityProblem == null, - "MySQL not reachable at " + config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost") - + ":" + config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306") + "MySQL not reachable at " + JdbcTestSupport.config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost") + + ":" + JdbcTestSupport.config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306") + " — skipping MySQL integration tests (" + reachabilityProblem + ")"); try (Connection connection = rootDataSource.getConnection(); Statement statement = connection.createStatement()) { @@ -238,28 +238,16 @@ class JdbcMysqlIntegrationTest { private static MysqlDataSource newDataSource(String database) { MysqlDataSource mysqlDataSource = new MysqlDataSource(); - String host = config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost"); - String port = config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306"); + String host = JdbcTestSupport.config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost"); + String port = JdbcTestSupport.config("ordo.test.mysql.port", "ORDO_TEST_MYSQL_PORT", "3306"); String databasePath = database.isEmpty() ? "/" : "/" + database; mysqlDataSource.setUrl("jdbc:mysql://" + host + ":" + port + databasePath + "?allowPublicKeyRetrieval=true&sslMode=DISABLED&characterEncoding=utf8"); - mysqlDataSource.setUser(config("ordo.test.mysql.user", "ORDO_TEST_MYSQL_USER", "root")); - mysqlDataSource.setPassword(config("ordo.test.mysql.password", "ORDO_TEST_MYSQL_PASSWORD", "root")); + mysqlDataSource.setUser(JdbcTestSupport.config("ordo.test.mysql.user", "ORDO_TEST_MYSQL_USER", "root")); + mysqlDataSource.setPassword(JdbcTestSupport.config("ordo.test.mysql.password", "ORDO_TEST_MYSQL_PASSWORD", "root")); 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, ApprovalTask completed, JdbcApprovalTaskRepository repository) { try { diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java index 796702f..27a2089 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java @@ -234,29 +234,17 @@ class JdbcPostgresIntegrationTest { private static PGSimpleDataSource newDataSource(String currentSchema) { PGSimpleDataSource pgDataSource = new PGSimpleDataSource(); - pgDataSource.setServerNames(new String[]{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.setDatabaseName(config("ordo.test.pg.database", "ORDO_TEST_PG_DATABASE", "postgres")); - pgDataSource.setUser(config("ordo.test.pg.user", "ORDO_TEST_PG_USER", "postgres")); - pgDataSource.setPassword(config("ordo.test.pg.password", "ORDO_TEST_PG_PASSWORD", "postgres")); + pgDataSource.setServerNames(new String[]{JdbcTestSupport.config("ordo.test.pg.host", "ORDO_TEST_PG_HOST", "localhost")}); + pgDataSource.setPortNumbers(new int[]{Integer.parseInt(JdbcTestSupport.config("ordo.test.pg.port", "ORDO_TEST_PG_PORT", "5432"))}); + pgDataSource.setDatabaseName(JdbcTestSupport.config("ordo.test.pg.database", "ORDO_TEST_PG_DATABASE", "postgres")); + pgDataSource.setUser(JdbcTestSupport.config("ordo.test.pg.user", "ORDO_TEST_PG_USER", "postgres")); + pgDataSource.setPassword(JdbcTestSupport.config("ordo.test.pg.password", "ORDO_TEST_PG_PASSWORD", "postgres")); if (!currentSchema.isEmpty()) { pgDataSource.setCurrentSchema(currentSchema); } 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, ApprovalTask completed, JdbcApprovalTaskRepository repository) { try { diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcTestSupport.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcTestSupport.java index ea041fc..ad024cb 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcTestSupport.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcTestSupport.java @@ -30,6 +30,18 @@ final class JdbcTestSupport { 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) { applySchema(dataSource, POSTGRES_BASELINE, POSTGRES_V2); }