diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/AbstractProxyCollection.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/AbstractProxyCollection.java new file mode 100644 index 00000000..23d0b940 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/AbstractProxyCollection.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.expressions; + +import java.util.Collection; + +abstract class AbstractProxyCollection { + + protected Collection values; + + protected AbstractProxyCollection(Collection values) { + this.values = values; + } + + public int size() { + return values.size(); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public boolean contains(Object o) { + return values.contains(o); + } + + public boolean remove(Object o) { + return values.remove(o); + } + + public boolean containsAll(Collection c) { + return values.containsAll(c); + } + + public boolean retainAll(Collection c) { + return values.retainAll(c); + } + + public boolean removeAll(Collection c) { + return values.removeAll(c); + } + + public void clear() { + values.clear(); + } + + public boolean addAll(Collection c) { + return values.addAll(c); + } + + public boolean add(T e) { + return values.add(e); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ObjectExpressionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ObjectExpressionFactory.java index 06ed1b17..2362246d 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ObjectExpressionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ObjectExpressionFactory.java @@ -19,6 +19,7 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowPredicate; +import java.util.Collection; import java.util.Map; public abstract class ObjectExpressionFactory extends AbstractExpressionFactory { @@ -31,13 +32,36 @@ protected ObjectExpression buildExpression(ExpressionDescriptor desc) { return expression::eval; } else if (desc.asObject() != null) { Object exprObj = buildExpressionObject(desc.asObject(), this); - return exprObj instanceof Map map - ? (w, t, n) -> evaluateExpressionMap(map, w, t, n) - : (w, t, n) -> desc.asObject(); + return (w, t, n) -> evaluateExpressionObject(exprObj, w, t, n); } throw new IllegalArgumentException("Both object and str are null"); } + private Object buildExpressionObject(Object obj, ExpressionFactory factory) { + if (obj instanceof ObjectExpression expr) { + return expr; + } else if (obj instanceof Map map) { + return buildExpressionMap(map, factory); + } else if (obj instanceof Collection col) { + return buildExpressionList(col, factory); + } else { + return obj; + } + } + + private Object evaluateExpressionObject( + Object obj, WorkflowContext workflow, TaskContext task, WorkflowModel model) { + if (obj instanceof ObjectExpression expr) { + return toJavaObject(expr.eval(workflow, task, model)); + } else if (obj instanceof Map map) { + return evaluateExpressionMap(map, workflow, task, model); + } else if (obj instanceof Collection col) { + return evaluateExpressionCollection(col, workflow, task, model); + } else { + return obj; + } + } + @Override public WorkflowPredicate buildPredicate(ExpressionDescriptor desc) { ObjectExpression expr = buildExpression(desc); @@ -51,22 +75,32 @@ protected Object toJavaObject(Object eval) { } private Map buildExpressionMap( - Map origMap, ExpressionFactory factory) { + Map map, ExpressionFactory factory) { return new ProxyMap( - origMap, o -> ExpressionUtils.isExpr(o) ? buildExpression(o.toString()) : o); + map, + o -> + ExpressionUtils.isExpr(o) + ? buildExpression(o.toString()) + : buildExpressionObject(o, factory)); } - private Object buildExpressionObject(Object obj, ExpressionFactory factory) { - return obj instanceof Map map ? buildExpressionMap(map, factory) : obj; + private Collection buildExpressionList( + Collection col, ExpressionFactory factory) { + return new ProxyCollection( + col, + o -> + ExpressionUtils.isExpr(o) + ? buildExpression(o.toString()) + : buildExpressionObject(o, factory)); } private Map evaluateExpressionMap( - Map origMap, WorkflowContext workflow, TaskContext task, WorkflowModel n) { - return new ProxyMap( - origMap, - o -> - o instanceof ObjectExpression - ? toJavaObject(((ObjectExpression) o).eval(workflow, task, n)) - : o); + Map map, WorkflowContext workflow, TaskContext task, WorkflowModel n) { + return new ProxyMap(map, o -> evaluateExpressionObject(o, workflow, task, n)); + } + + private Collection evaluateExpressionCollection( + Collection col, WorkflowContext workflow, TaskContext task, WorkflowModel n) { + return new ProxyCollection(col, o -> evaluateExpressionObject(o, workflow, task, n)); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyCollection.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyCollection.java new file mode 100644 index 00000000..c2518bfe --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyCollection.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.expressions; + +import java.util.Collection; +import java.util.Iterator; +import java.util.function.UnaryOperator; + +class ProxyCollection extends AbstractProxyCollection implements Collection { + + private final UnaryOperator function; + + public ProxyCollection(Collection values, UnaryOperator function) { + super(values); + this.function = function; + } + + @Override + public Iterator iterator() { + return new ProxyIterator(values.iterator(), function); + } + + @Override + public Object[] toArray() { + return processArray(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processArray(values.toArray(a)); + } + + private S[] processArray(S[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (S) function.apply(array[i]); + } + return array; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyIterator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyIterator.java new file mode 100644 index 00000000..bd63c2c5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyIterator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.expressions; + +import java.util.Iterator; +import java.util.function.UnaryOperator; + +class ProxyIterator implements Iterator { + + private Iterator iter; + private final UnaryOperator function; + + public ProxyIterator(Iterator iter, UnaryOperator function) { + this.iter = iter; + this.function = function; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Object next() { + return function.apply(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java index bf4464b2..9ebfe214 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java @@ -83,7 +83,7 @@ public Set keySet() { @Override public Collection values() { - return new ProxyCollection(map.values()); + return new ProxyCollection(map.values(), function); } @Override @@ -91,55 +91,6 @@ public Set> entrySet() { return new ProxyEntrySet(map.entrySet()); } - private abstract class AbstractProxyCollection { - - protected Collection values; - - protected AbstractProxyCollection(Collection values) { - this.values = values; - } - - public int size() { - return values.size(); - } - - public boolean isEmpty() { - return values.isEmpty(); - } - - public boolean contains(Object o) { - return values.contains(o); - } - - public boolean remove(Object o) { - return values.remove(o); - } - - public boolean containsAll(Collection c) { - return values.containsAll(c); - } - - public boolean retainAll(Collection c) { - return values.retainAll(c); - } - - public boolean removeAll(Collection c) { - return values.removeAll(c); - } - - public void clear() { - values.clear(); - } - - public boolean addAll(Collection c) { - return values.addAll(c); - } - - public boolean add(T e) { - return values.add(e); - } - } - private class ProxyEntrySet extends AbstractProxyCollection> implements Set> { @@ -170,36 +121,6 @@ private T[] processEntries(T[] array) { } } - private class ProxyCollection extends AbstractProxyCollection - implements Collection { - - public ProxyCollection(Collection values) { - super(values); - } - - @Override - public Iterator iterator() { - return new ProxyIterator(values.iterator()); - } - - @Override - public Object[] toArray() { - return processArray(values.toArray()); - } - - @Override - public T[] toArray(T[] a) { - return processArray(values.toArray(a)); - } - - private S[] processArray(S[] array) { - for (int i = 0; i < array.length; i++) { - array[i] = (S) processValue(array[i]); - } - return array; - } - } - private class ProxyEntry implements Entry { private Entry entry; @@ -224,30 +145,6 @@ public Object setValue(Object value) { } } - private class ProxyIterator implements Iterator { - - private Iterator iter; - - public ProxyIterator(Iterator iter) { - this.iter = iter; - } - - @Override - public boolean hasNext() { - return iter.hasNext(); - } - - @Override - public Object next() { - return processValue(iter.next()); - } - - @Override - public void remove() { - iter.remove(); - } - } - private class ProxyEntryIterator implements Iterator> { private Iterator> iter; diff --git a/impl/jackson/pom.xml b/impl/jackson/pom.xml index 0ed7816f..d0a1f90c 100644 --- a/impl/jackson/pom.xml +++ b/impl/jackson/pom.xml @@ -28,5 +28,17 @@ net.thisptr jackson-jq + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + + + org.mockito + mockito-core + \ No newline at end of file diff --git a/impl/jackson/src/test/java/io/serverlessworkflow/impl/expressions/jq/JQExpressionFactoryTest.java b/impl/jackson/src/test/java/io/serverlessworkflow/impl/expressions/jq/JQExpressionFactoryTest.java new file mode 100644 index 00000000..4a72893c --- /dev/null +++ b/impl/jackson/src/test/java/io/serverlessworkflow/impl/expressions/jq/JQExpressionFactoryTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.expressions.jq; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowValueResolver; +import io.serverlessworkflow.impl.expressions.ExpressionDescriptor; +import io.serverlessworkflow.impl.jackson.JsonUtils; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class JQExpressionFactoryTest { + + @Test + void testNestedMap() { + WorkflowContext workflowContext = Mockito.mock(WorkflowContext.class); + JQExpressionFactory factory = new JQExpressionFactory(); + WorkflowValueResolver> expr = + factory.resolveMap( + ExpressionDescriptor.object( + Map.of( + "name", + "${.name}", + "surname", + "Doe", + "nested", + Map.of("name", "${.name}", "surname", "Doe")))); + Map result = + expr.apply( + workflowContext, + null, + new JacksonModel(JsonUtils.mapper().createObjectNode().put("name", "John"))); + assertThat(result.get("name")).isEqualTo("John"); + assertThat(result.get("surname")).isEqualTo("Doe"); + Map nested = (Map) result.get("nested"); + assertThat(result.get("name")).isEqualTo("John"); + assertThat(result.get("surname")).isEqualTo("Doe"); + } + + @Test + void testNestedArray() { + WorkflowContext workflowContext = Mockito.mock(WorkflowContext.class); + JQExpressionFactory factory = new JQExpressionFactory(); + WorkflowValueResolver> expr = + factory.resolveMap( + ExpressionDescriptor.object( + Map.of("array", "${.array}", "nested", Map.of("array", "${.array}")))); + Map result = + expr.apply( + workflowContext, + null, + new JacksonModel( + JsonUtils.mapper() + .createObjectNode() + .set("array", JsonUtils.mapper().createArrayNode().add("John").add("Doe")))); + assertThat(result.get("array")).isEqualTo(List.of("John", "Doe")); + Map nested = (Map) result.get("nested"); + assertThat(nested.get("array")).isEqualTo(List.of("John", "Doe")); + } + + @Test + void testList() { + WorkflowContext workflowContext = Mockito.mock(WorkflowContext.class); + JQExpressionFactory factory = new JQExpressionFactory(); + WorkflowValueResolver> expr = + factory.resolveMap( + ExpressionDescriptor.object(Map.of("array", List.of("item1", "item2", "${.name}")))); + Map result = + expr.apply( + workflowContext, + null, + new JacksonModel(JsonUtils.mapper().createObjectNode().put("name", "item3"))); + Iterator iter = ((Collection) result.get("array")).iterator(); + assertThat(iter.next()).isEqualTo("item1"); + assertThat(iter.next()).isEqualTo("item2"); + assertThat(iter.next()).isEqualTo("item3"); + } +}