From 515ad2e6a5fb50434b81f20c3ee5f68c012f6eb1 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Fri, 8 May 2026 15:14:56 +0200 Subject: [PATCH] [Fix #1372] conversion to no collection type forWorkflowModelCollection Signed-off-by: fjtirado --- .../fluent/test/FuncEventFilterTest.java | 121 +++++++++++++----- .../impl/CollectionConversionUtils.java | 65 +++++++--- .../impl/jackson/JsonUtils.java | 31 +++++ .../model/jackson/JacksonModelCollection.java | 5 +- 4 files changed, 163 insertions(+), 59 deletions(-) diff --git a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncEventFilterTest.java b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncEventFilterTest.java index 44447baa3..dbac8eda7 100644 --- a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncEventFilterTest.java +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncEventFilterTest.java @@ -32,12 +32,14 @@ import io.cloudevents.core.builder.CloudEventBuilder; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.fluent.func.dsl.ListenStep; import io.serverlessworkflow.impl.TaskContextData; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContextData; import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowInstance; import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowModelCollection; import io.serverlessworkflow.impl.WorkflowStatus; import io.serverlessworkflow.impl.events.EventPublisher; import java.net.URI; @@ -107,6 +109,36 @@ void testListenToOneArray() { .build()); } + @Test + void testPrimitiveArray() { + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + Workflow workflow = + FuncWorkflowBuilder.workflow("doubleArray") + .tasks(function(FuncEventFilterTest::doubleArray)) + .build(); + WorkflowModelCollection col = app.modelFactory().createCollection(); + col.add(app.modelFactory().from(1)); + col.add(app.modelFactory().from(2)); + col.add(app.modelFactory().from(3)); + assertThat( + app.workflowDefinition(workflow) + .instance(col) + .start() + .join() + .as(int[].class) + .orElseThrow()) + .isEqualTo(new int[] {2, 4, 6}); + } + } + + private static int[] doubleArray(int[] input) { + int[] output = new int[input.length]; + for (int i = 0; i < input.length; i++) { + output[i] = input[i] << 1; + } + return output; + } + private Workflow reviewEmitter() { return FuncWorkflowBuilder.workflow("emitReview") .tasks(emitJson("draftReady", "org.acme.test.review", Review.class)) @@ -141,42 +173,63 @@ void sendEmail(NewsletterDraft draft) { } @Test - void testJacksonAutomagicalConversion() throws Exception { - try (WorkflowApplication app = WorkflowApplication.builder().build()) { + void testAutomaticConversion() throws Exception { + testConversionWorkflow( + listen( + "waitHumanReview", + to().one( + consumed("org.acme.newsletter.review.done") + .extensionByInstanceId("instanceid")))); + } - Workflow workflow = - FuncWorkflowBuilder.workflow("intelligent-newsletter") - .tasks( - function("draftAgent", this::writeDraft).exportAsTaskOutput(), - emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class), - listen( - "waitHumanReview", - to().one( - consumed("org.acme.newsletter.review.done") - .extensionByInstanceId("instanceid"))) - .outputAs((Collection events) -> events.iterator().next()), - // The engine sees the incoming JsonNode, sees this task expects - // HumanReview.class, - // and natively deserializes it for you before executing the lambda! - switchWhenOrElse( - h -> HumanReview.NEEDS_REVISION.equals(h.status()), - "humanEditorAgent", - "sendNewsletter", - HumanReview.class), - function("humanEditorAgent", this::editDraft) - .exportAsTaskOutput() - .then("draftReady"), - consume("sendNewsletter", this::sendEmail) - // Because we are in Jackson, the payload at this evaluation stage can be a - // Map. - // We simply check for the "status" field to know if it's the review payload. - .inputFrom( - (Map payload, - WorkflowContextData wfc, - TaskContextData tfc) -> - payload.containsKey("status") ? wfc.context() : payload)) - .build(); + @Test + void testCollectionConversion() throws Exception { + testConversionWorkflow( + listen( + to().one( + consumed("org.acme.newsletter.review.done") + .extensionByInstanceId("instanceid"))) + .outputAs((Collection col) -> col.iterator().next())); + } + @Test + void testNodeConversion() throws Exception { + testConversionWorkflow( + listen( + "waitHumanReview", + to().one( + consumed("org.acme.newsletter.review.done") + .extensionByInstanceId("instanceid"))) + .outputAs((ArrayNode col) -> col.get(0))); + } + + private void testConversionWorkflow(ListenStep listen) throws Exception { + Workflow workflow = + FuncWorkflowBuilder.workflow("intelligent-newsletter") + .tasks( + function("draftAgent", this::writeDraft).exportAsTaskOutput(), + emitJson("draftReady", "org.acme.email.review.required", NewsletterDraft.class), + listen, + switchWhenOrElse( + h -> HumanReview.NEEDS_REVISION.equals(h.status()), + "humanEditorAgent", + "sendNewsletter", + HumanReview.class), + function("humanEditorAgent", this::editDraft) + .exportAsTaskOutput() + .then("draftReady"), + consume("sendNewsletter", this::sendEmail) + // Because we are in Jackson, the payload at this evaluation stage can be a + // Map. + // We simply check for the "status" field to know if it's the review payload. + .inputFrom( + (Map payload, + WorkflowContextData wfc, + TaskContextData tfc) -> + payload.containsKey("status") ? wfc.context() : payload)) + .build(); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { WorkflowDefinition definition = app.workflowDefinition(workflow); WorkflowInstance instance = definition.instance(new NewsletterRequest("Tech Stocks")); CompletableFuture future = instance.start(); diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/CollectionConversionUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/CollectionConversionUtils.java index 2ebe7a1c8..5d3544ac6 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/CollectionConversionUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/CollectionConversionUtils.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; @@ -28,39 +29,61 @@ public final class CollectionConversionUtils { private CollectionConversionUtils() {} /** - * Safely converts a base Collection into the requested List, Set, or Array type. + * Safely converts an Iterable into the requested type. * - * @param elements The base collection of elements. + * @param elements Iterable containing the elements to be converted. * @param clazz The target class to convert to. - * @param primitiveConverter Strategy for converting items to primitives if an array is requested. + * @param converter Convert items to class if requested. */ public static Optional as( - Collection elements, - Class clazz, - BiFunction, Object> primitiveConverter) { + Iterable elements, Class clazz, BiFunction, Object> converter) { if (clazz.isAssignableFrom(List.class)) - return Optional.of(clazz.cast(new ArrayList<>(elements))); + return Optional.of(clazz.cast(iterableToCollection(elements, new ArrayList<>()))); else if (clazz.isAssignableFrom(Set.class)) - return Optional.of(clazz.cast(new HashSet<>(elements))); - - if (clazz.isArray()) { + return Optional.of(clazz.cast(iterableToCollection(elements, new HashSet<>()))); + else if (clazz.isArray()) { Class componentType = clazz.getComponentType(); - - if (!componentType.isPrimitive()) { - Object[] typedArray = (Object[]) Array.newInstance(componentType, 0); - return Optional.of(clazz.cast(elements.toArray(typedArray))); - } - - Object primitiveArray = Array.newInstance(componentType, elements.size()); + Collection collection = iterableToCollection(elements); + Object primitiveArray = Array.newInstance(componentType, collection.size()); int i = 0; - for (Object item : elements) - Array.set(primitiveArray, i++, primitiveConverter.apply(item, componentType)); - + for (Object item : collection) { + Array.set( + primitiveArray, + i++, + convert(item, componentType, converter) + .orElseThrow( + () -> + new IllegalArgumentException( + "Cannot convert " + item + " into class " + componentType))); + } return Optional.of(clazz.cast(primitiveArray)); + } else { + Iterator iter = elements.iterator(); + return iter.hasNext() ? convert(iter.next(), clazz, converter) : Optional.empty(); } + } + + private static Optional convert( + Object obj, Class clazz, BiFunction, Object> converter) { + if (obj instanceof WorkflowModel model) { + return model.as(clazz); + } else { + Object converted = converter.apply(obj, clazz); + if (clazz.isPrimitive()) { + return (Optional) Optional.of(converted); + } + return clazz.isInstance(converted) ? Optional.of(clazz.cast(converted)) : Optional.empty(); + } + } + + private static Collection iterableToCollection(Iterable t, Collection c) { + t.forEach(c::add); + return c; + } - return Optional.empty(); + private static Collection iterableToCollection(Iterable t) { + return t instanceof Collection col ? col : iterableToCollection(t, new ArrayList<>()); } /** diff --git a/impl/json-utils/src/main/java/io/serverlessworkflow/impl/jackson/JsonUtils.java b/impl/json-utils/src/main/java/io/serverlessworkflow/impl/jackson/JsonUtils.java index 236178d35..c3bb0bed6 100644 --- a/impl/json-utils/src/main/java/io/serverlessworkflow/impl/jackson/JsonUtils.java +++ b/impl/json-utils/src/main/java/io/serverlessworkflow/impl/jackson/JsonUtils.java @@ -206,12 +206,43 @@ public static T convertValue(JsonNode jsonNode, Class returnType) { obj = JacksonCloudEventUtils.toCloudEvent(jsonNode); } else if (CloudEventData.class.isAssignableFrom(returnType)) { obj = JacksonCloudEventUtils.toCloudEventData(jsonNode); + } else if (returnType.isPrimitive()) { + return (T) convertPrimitive(jsonNode, returnType); + } else if (Short.class.isAssignableFrom(returnType)) { + obj = Short.valueOf((short) jsonNode.asInt()); + } else if (Float.class.isAssignableFrom(returnType)) { + obj = Float.valueOf((float) jsonNode.asDouble()); + } else if (Character.class.isAssignableFrom(returnType)) { + obj = Character.valueOf((char) jsonNode.asInt()); + } else if (Byte.class.isAssignableFrom(returnType)) { + obj = Byte.valueOf((byte) jsonNode.asInt()); } else { obj = mapper().convertValue(jsonNode, returnType); } return returnType.cast(obj); } + private static Object convertPrimitive(JsonNode jsonNode, Class returnType) { + if (boolean.class.equals(returnType)) { + return jsonNode.asBoolean(); + } else if (int.class.isAssignableFrom(returnType)) { + return jsonNode.asInt(); + } else if (double.class.isAssignableFrom(returnType)) { + return jsonNode.asDouble(); + } else if (long.class.isAssignableFrom(returnType)) { + return jsonNode.asLong(); + } else if (short.class.isAssignableFrom(returnType)) { + return (short) jsonNode.asInt(); + } else if (float.class.isAssignableFrom(returnType)) { + return (float) jsonNode.asDouble(); + } else if (char.class.isAssignableFrom(returnType)) { + return (char) jsonNode.asInt(); + } else if (byte.class.isAssignableFrom(returnType)) { + return (byte) jsonNode.asInt(); + } + throw new IllegalStateException("There is a unknown primitive!!!" + returnType); + } + public static Object simpleToJavaValue(JsonNode jsonNode) { return internalToJavaValue(jsonNode, node -> node, node -> node); } diff --git a/impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModelCollection.java b/impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModelCollection.java index 76aaab65e..1796efca0 100644 --- a/impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModelCollection.java +++ b/impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModelCollection.java @@ -51,10 +51,7 @@ public Optional as(Class clazz) { if (clazz.isInstance(node)) return Optional.of(clazz.cast(node)); if (clazz.isInstance(this)) return Optional.of(clazz.cast(this)); - List elements = new ArrayList<>(node.size()); - node.forEach(elements::add); - - return CollectionConversionUtils.as(elements, clazz, JsonUtils::convertValue); + return CollectionConversionUtils.as(node, clazz, JsonUtils::convertValue); } @Override