diff --git a/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/MainTest.java b/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/MainTest.java index 035b479b9b..f553ca61b0 100644 --- a/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/MainTest.java +++ b/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/MainTest.java @@ -1746,6 +1746,20 @@ public void flowCallOutExpressionCompat() throws Exception { assertLog(runtime.allLogs(), ".*" + Pattern.quote("out as map with loop: [abc_0, abc_1]") + ".*"); } + @Test + public void prefixedFunctionsInExpressions() throws Exception { + deploy("prefixedFunctions"); + + save(ProcessConfiguration.builder() + .putArguments("name", "world") + .build()); + + run(); + + assertLog(runtime.allLogs(), ".*Hi, world!.*"); + assertLog(runtime.allLogs(), ".*Hello, world!.*"); + } + private void deploy(String name) throws URISyntaxException, IOException { runtime.deploy(name); } diff --git a/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/functions/TestFunction.java b/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/functions/TestFunction.java new file mode 100644 index 0000000000..794c8c54a4 --- /dev/null +++ b/runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/functions/TestFunction.java @@ -0,0 +1,19 @@ +package com.walmartlabs.concord.runtime.v2.runner.functions; + +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; + +import javax.inject.Named; + +@Named +public class TestFunction { + + @ELFunction("testGreet") + public static String regularGreet(String name) { + return "Hi, " + name + "!"; + } + + @ELFunction("testFunction:greet") + public static String prefixedGreet(String name) { + return "Hello, " + name + "!"; + } +} diff --git a/runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/prefixedFunctions/concord.yml b/runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/prefixedFunctions/concord.yml new file mode 100644 index 0000000000..d7a927e23c --- /dev/null +++ b/runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/prefixedFunctions/concord.yml @@ -0,0 +1,4 @@ +flows: + default: + - log: "${testGreet(name)}" + - log: "${testFunction:greet(name)}" diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/DefaultExpressionEvaluator.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/DefaultExpressionEvaluator.java index 037d2163ee..bf79540fc5 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/DefaultExpressionEvaluator.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/DefaultExpressionEvaluator.java @@ -36,10 +36,11 @@ public class DefaultExpressionEvaluator implements ExpressionEvaluator { @Inject public DefaultExpressionEvaluator(TaskProviders taskProviders, + FunctionHolder functionHolder, List taskMethodResolvers, List beanMethodResolvers, SensitiveDataProcessor sensitiveDataProcessor) { - this.delegate = new LazyExpressionEvaluator(taskProviders, taskMethodResolvers, beanMethodResolvers, sensitiveDataProcessor); + this.delegate = new LazyExpressionEvaluator(taskProviders, functionHolder, taskMethodResolvers, beanMethodResolvers, sensitiveDataProcessor); } @Override diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionMapper.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionHolder.java similarity index 62% rename from runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionMapper.java rename to runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionHolder.java index 37583f4bf5..0035fc296f 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionMapper.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionHolder.java @@ -22,21 +22,23 @@ import java.lang.reflect.Method; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; -public class FunctionMapper extends javax.el.FunctionMapper { +public class FunctionHolder { - private final Map functions; + private final Map functions = new ConcurrentHashMap<>(); - public FunctionMapper(Map functions) { - this.functions = functions; + public FunctionHolder register(String name, Method method) { + functions.put(name, method); + return this; } - @Override - public Method resolveFunction(String prefix, String localName) { - if (prefix == null || prefix.trim().isEmpty()) { - return functions.get(localName); - } + public Method resolve(String name) { + return functions.get(name); + } - return functions.get(prefix + ":" + localName); + public Set names() { + return Set.copyOf(functions.keySet()); } } diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyExpressionEvaluator.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyExpressionEvaluator.java index 50e813f97e..c3e5fdd36f 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyExpressionEvaluator.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyExpressionEvaluator.java @@ -22,9 +22,8 @@ import com.walmartlabs.concord.common.ConfigurationUtils; import com.walmartlabs.concord.common.ExceptionUtils; -import com.walmartlabs.concord.runtime.v2.runner.el.functions.*; -import com.walmartlabs.concord.runtime.v2.runner.el.resolvers.MapELResolver; import com.walmartlabs.concord.runtime.v2.runner.el.resolvers.*; +import com.walmartlabs.concord.runtime.v2.runner.el.resolvers.MapELResolver; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders; import com.walmartlabs.concord.runtime.v2.runner.vm.WrappedException; import com.walmartlabs.concord.runtime.v2.sdk.*; @@ -50,12 +49,13 @@ public class LazyExpressionEvaluator implements ExpressionEvaluator { private final SensitiveDataProcessor sensitiveDataProcessor; public LazyExpressionEvaluator(TaskProviders taskProviders, + FunctionHolder functionHolder, List taskMethodResolvers, List beanMethodResolvers, SensitiveDataProcessor sensitiveDataProcessor) { this.taskProviders = taskProviders; this.sensitiveDataProcessor = sensitiveDataProcessor; - this.functionMapper = createFunctionMapper(); + this.functionMapper = new DelegatingFunctionMapper(functionHolder); this.taskMethodResolvers = taskMethodResolvers; this.beanMethodResolvers = beanMethodResolvers; } @@ -203,22 +203,6 @@ private ELResolver createResolver(LazyEvalContext evalContext, return r; } - private static FunctionMapper createFunctionMapper() { - var functions = new HashMap(); - functions.put("hasVariable", HasVariableFunction.getMethod()); - functions.put("hasNonNullVariable", HasNonNullVariableFunction.getMethod()); - functions.put("orDefault", OrDefaultFunction.getMethod()); - functions.put("allVariables", AllVariablesFunction.getMethod()); - functions.put("currentFlowName", CurrentFlowNameFunction.getMethod()); - functions.put("evalAsMap", EvalAsMapFunction.getMethod()); - functions.put("isDebug", IsDebugFunction.getMethod()); - functions.put("throw", ThrowFunction.getMethod()); - functions.put("hasFlow", HasFlowFunction.getMethod()); - functions.put("uuid", UuidFunction.getMethod()); - functions.put("isDryRun", IsDryRunFunction.getMethod()); - return new FunctionMapper(functions); - } - private static boolean hasExpression(String s) { return s.contains("${"); } @@ -272,4 +256,20 @@ private static Optional propertyNameFromException(PropertyNotFoundExcept return Optional.empty(); } + + private static class DelegatingFunctionMapper extends FunctionMapper { + final FunctionHolder functionHolder; + + DelegatingFunctionMapper(FunctionHolder functionHolder) { + this.functionHolder = functionHolder; + } + + @Override + public Method resolveFunction(String prefix, String localName) { + if (prefix != null && !prefix.isBlank()) { + return functionHolder.resolve(prefix + ":" + localName); + } + return functionHolder.resolve(localName); + } + } } diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/AllVariablesFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/AllVariablesFunction.java index 097f8628a7..7b3da15816 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/AllVariablesFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/AllVariablesFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,14 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; -import java.lang.reflect.Method; import java.util.Map; public final class AllVariablesFunction { - public static Method getMethod() { - try { - return AllVariablesFunction.class.getMethod("allVariables"); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static Map allVariables() { return ThreadLocalEvalContext.get() .variables() diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/CurrentFlowNameFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/CurrentFlowNameFunction.java index bf3003d935..361b0ca459 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/CurrentFlowNameFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/CurrentFlowNameFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,13 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Context; -import java.lang.reflect.Method; - public final class CurrentFlowNameFunction { - public static Method getMethod() { - try { - return CurrentFlowNameFunction.class.getMethod("currentFlowName"); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static String currentFlowName() { Context ctx = ThreadLocalEvalContext.get().context(); if (ctx == null) { diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/EvalAsMapFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/EvalAsMapFunction.java index a1518070b8..5ff06a4e54 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/EvalAsMapFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/EvalAsMapFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,13 @@ * ===== */ -import com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory; -import com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Context; +import com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory; +import com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator; import com.walmartlabs.concord.svm.Runtime; -import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -34,14 +34,7 @@ public final class EvalAsMapFunction { - public static Method getMethod() { - try { - return EvalAsMapFunction.class.getMethod("evalAsMap", Object.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction @SuppressWarnings("unchecked") public static Map evalAsMap(Object value) { Context ctx = ThreadLocalEvalContext.get().context(); diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasFlowFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasFlowFunction.java index b261c3b9f3..40a2f284a5 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasFlowFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasFlowFunction.java @@ -22,21 +22,13 @@ import com.walmartlabs.concord.runtime.v2.model.ProcessDefinition; import com.walmartlabs.concord.runtime.v2.model.Profile; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Context; -import java.lang.reflect.Method; - public final class HasFlowFunction { - public static Method getMethod() { - try { - return HasFlowFunction.class.getMethod("hasFlow", String.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static boolean hasFlow(String name) { if (name == null || name.trim().isEmpty()) { return false; diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasNonNullVariableFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasNonNullVariableFunction.java index ff717a0f51..9e7a065cd3 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasNonNullVariableFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasNonNullVariableFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,25 +21,18 @@ */ import com.walmartlabs.concord.common.ConfigurationUtils; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Variables; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; public final class HasNonNullVariableFunction { - public static Method getMethod() { - try { - return HasNonNullVariableFunction.class.getMethod("hasVariable", String.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction @SuppressWarnings("unchecked") - public static boolean hasVariable(String name) { + public static boolean hasNonNullVariable(String name) { if (name == null || name.trim().isEmpty()) { return false; } diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasVariableFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasVariableFunction.java index 8abc89f6f1..878fc8f4d6 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasVariableFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasVariableFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,23 +21,16 @@ */ import com.walmartlabs.concord.common.ConfigurationUtils; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Variables; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; public final class HasVariableFunction { - public static Method getMethod() { - try { - return HasVariableFunction.class.getMethod("hasVariable", String.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction @SuppressWarnings("unchecked") public static boolean hasVariable(String name) { if (name == null || name.trim().isEmpty()) { diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDebugFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDebugFunction.java index 06feaee65f..8ca4b9744e 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDebugFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDebugFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,13 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Context; -import java.lang.reflect.Method; - public final class IsDebugFunction { - public static Method getMethod() { - try { - return IsDebugFunction.class.getMethod("isDebug"); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static boolean isDebug() { Context ctx = ThreadLocalEvalContext.get().context(); if (ctx == null) { diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDryRunFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDryRunFunction.java index 3c1b1d03f2..f54e378664 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDryRunFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDryRunFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2024 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,13 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Context; -import java.lang.reflect.Method; - public class IsDryRunFunction { - public static Method getMethod() { - try { - return IsDryRunFunction.class.getMethod("isDryRun"); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static boolean isDryRun() { Context ctx = ThreadLocalEvalContext.get().context(); if (ctx == null) { diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/OrDefaultFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/OrDefaultFunction.java index 9cbd892dde..45ea7bd3c8 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/OrDefaultFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/OrDefaultFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,23 +21,16 @@ */ import com.walmartlabs.concord.common.ConfigurationUtils; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext; import com.walmartlabs.concord.runtime.v2.sdk.Variables; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; public final class OrDefaultFunction { - public static Method getMethod() { - try { - return OrDefaultFunction.class.getMethod("orDefault", String.class, Object.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction @SuppressWarnings("unchecked") public static T orDefault(String variableName, T defaultValue) { boolean has = HasVariableFunction.hasVariable(variableName); diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/ThrowFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/ThrowFunction.java index 0d5238fa77..b1c73cba33 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/ThrowFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/ThrowFunction.java @@ -4,7 +4,7 @@ * ***** * Concord * ----- - * Copyright (C) 2017 - 2020 Walmart Inc. + * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,20 +20,12 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; import com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException; -import java.lang.reflect.Method; - public final class ThrowFunction { - public static Method getMethod() { - try { - return ThrowFunction.class.getMethod("throwError", String.class); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction("throw") public static Object throwError(String message) { throw new UserDefinedException(message); } diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/UuidFunction.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/UuidFunction.java index 2da97e2cb1..9e256fc23d 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/UuidFunction.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/UuidFunction.java @@ -20,19 +20,13 @@ * ===== */ -import java.lang.reflect.Method; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; + import java.util.UUID; public final class UuidFunction { - public static Method getMethod() { - try { - return UuidFunction.class.getMethod("uuid"); - } catch (Exception e) { - throw new RuntimeException("Method not found"); - } - } - + @ELFunction public static String uuid() { return UUID.randomUUID().toString(); } diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/BaseRunnerModule.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/BaseRunnerModule.java index 1735424232..bd5c2453ef 100644 --- a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/BaseRunnerModule.java +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/BaseRunnerModule.java @@ -29,7 +29,6 @@ import com.walmartlabs.concord.runtime.v2.runner.compiler.DefaultCompiler; import com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory; import com.walmartlabs.concord.runtime.v2.runner.context.DefaultContextFactory; -import com.walmartlabs.concord.runtime.v2.runner.el.DefaultExpressionEvaluator; import com.walmartlabs.concord.runtime.v2.runner.el.EvalContextFactoryImpl; import com.walmartlabs.concord.runtime.v2.sdk.*; import com.walmartlabs.concord.runtime.v2.runner.script.DefaultScriptEvaluator; @@ -44,12 +43,13 @@ public class BaseRunnerModule extends AbstractModule { @Override protected void configure() { + install(new ExpressionSupportModule()); + bind(ContextFactory.class).to(DefaultContextFactory.class); bind(FileService.class).to(DefaultFileService.class); bind(Compiler.class).to(DefaultCompiler.class); bind(PolicyEngine.class).toProvider(PolicyEngineProvider.class); bind(SynchronizationService.class).to(DefaultSynchronizationService.class); - bind(ExpressionEvaluator.class).to(DefaultExpressionEvaluator.class); bind(EvalContextFactory.class).to(EvalContextFactoryImpl.class); bind(ScriptEvaluator.class).to(DefaultScriptEvaluator.class); bind(ResourceResolver.class).to(DefaultResourceResolver.class); diff --git a/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ExpressionSupportModule.java b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ExpressionSupportModule.java new file mode 100644 index 0000000000..2371b2f15a --- /dev/null +++ b/runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ExpressionSupportModule.java @@ -0,0 +1,91 @@ +package com.walmartlabs.concord.runtime.v2.runner.guice; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * 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. + * ===== + */ + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; +import com.walmartlabs.concord.runtime.v2.runner.el.DefaultExpressionEvaluator; +import com.walmartlabs.concord.runtime.v2.sdk.ELFunction; +import com.walmartlabs.concord.runtime.v2.runner.el.FunctionHolder; +import com.walmartlabs.concord.runtime.v2.runner.el.functions.*; +import com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator; + +import java.lang.reflect.Modifier; + +public class ExpressionSupportModule implements Module { + + @Override + public void configure(Binder binder) { + binder.bind(ExpressionEvaluator.class).to(DefaultExpressionEvaluator.class); + + // support for @ELFunction + var functionHolder = new FunctionHolder(); + binder.bind(FunctionHolder.class).toInstance(functionHolder); + binder.bindListener(Matchers.any(), new TypeListener() { + @Override + public void hear(TypeLiteral type, TypeEncounter encounter) { + var clazz = type.getRawType(); + + for (var method : clazz.getDeclaredMethods()) { + var annotation = method.getAnnotation(ELFunction.class); + if (annotation == null) { + continue; + } + + if (!Modifier.isStatic(method.getModifiers())) { + var msg = String.format("@ELFunction method must be static: %s.%s", clazz.getName(), method.getName()); + encounter.addError(msg); + continue; + } + + if (!Modifier.isPublic(method.getModifiers())) { + var msg = String.format("@ELFunction method must be public: %s.%s", clazz.getName(), method.getName()); + encounter.addError(msg); + continue; + } + + var name = annotation.value(); + if (name == null || name.isBlank()) { + name = method.getName(); + } + functionHolder.register(name, method); + } + } + }); + + // built-in functions + binder.bind(AllVariablesFunction.class).asEagerSingleton(); + binder.bind(CurrentFlowNameFunction.class).asEagerSingleton(); + binder.bind(EvalAsMapFunction.class).asEagerSingleton(); + binder.bind(HasFlowFunction.class).asEagerSingleton(); + binder.bind(HasNonNullVariableFunction.class).asEagerSingleton(); + binder.bind(HasVariableFunction.class).asEagerSingleton(); + binder.bind(IsDebugFunction.class).asEagerSingleton(); + binder.bind(IsDryRunFunction.class).asEagerSingleton(); + binder.bind(OrDefaultFunction.class).asEagerSingleton(); + binder.bind(ThrowFunction.class).asEagerSingleton(); + binder.bind(UuidFunction.class).asEagerSingleton(); + } +} diff --git a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ExpressionEvaluatorTest.java b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ExpressionEvaluatorTest.java index 477886d3e4..9c47ad5f71 100644 --- a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ExpressionEvaluatorTest.java +++ b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ExpressionEvaluatorTest.java @@ -20,6 +20,8 @@ * ===== */ +import com.walmartlabs.concord.runtime.v2.runner.el.functions.AllVariablesFunction; +import com.walmartlabs.concord.runtime.v2.runner.el.functions.HasVariableFunction; import com.walmartlabs.concord.runtime.v2.runner.el.resolvers.SensitiveDataProcessor; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders; import com.walmartlabs.concord.runtime.v2.sdk.*; @@ -397,7 +399,14 @@ private static Map map(Object... values) { } private static ExpressionEvaluator expressionEvaluator(TaskProviders taskProviders) { - return new DefaultExpressionEvaluator(taskProviders, List.of(), List.of(), mock(SensitiveDataProcessor.class)); + try { + var functions = new FunctionHolder() + .register("allVariables", AllVariablesFunction.class.getMethod("allVariables")) + .register("hasVariable", HasVariableFunction.class.getMethod("hasVariable", String.class)); + return new DefaultExpressionEvaluator(taskProviders, functions, List.of(), List.of(), mock(SensitiveDataProcessor.class)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } } public static class TestTask implements Task { diff --git a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasNoNullVariableFunctionTest.java b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasNoNullVariableFunctionTest.java index a3d4608a9f..cdb0c7c1f0 100644 --- a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasNoNullVariableFunctionTest.java +++ b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasNoNullVariableFunctionTest.java @@ -22,7 +22,6 @@ import com.walmartlabs.concord.common.ConfigurationUtils; import com.walmartlabs.concord.runtime.v2.runner.el.functions.HasNonNullVariableFunction; -import com.walmartlabs.concord.runtime.v2.runner.el.functions.HasVariableFunction; import com.walmartlabs.concord.runtime.v2.sdk.Context; import com.walmartlabs.concord.runtime.v2.sdk.EvalContext; import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; @@ -40,23 +39,23 @@ public class HasNoNullVariableFunctionTest { @Test public void test() { withVariables(Collections.singletonMap("test", "123"), () -> { - assertTrue(HasNonNullVariableFunction.hasVariable("test")); - assertFalse(HasNonNullVariableFunction.hasVariable("test.nested.deep")); - assertFalse(HasNonNullVariableFunction.hasVariable("boom")); + assertTrue(HasNonNullVariableFunction.hasNonNullVariable("test")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("test.nested.deep")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("boom")); }); withVariables(Collections.singletonMap("testNull", null), () -> { - assertFalse(HasNonNullVariableFunction.hasVariable("testNull")); - assertFalse(HasNonNullVariableFunction.hasVariable("testNull.k2")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("testNull")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("testNull.k2")); }); withVariables(Collections.singletonMap("a", ConfigurationUtils.toNested("b.c", "123")), () -> { - assertTrue(HasNonNullVariableFunction.hasVariable("a")); - assertTrue(HasNonNullVariableFunction.hasVariable("a.b")); - assertTrue(HasNonNullVariableFunction.hasVariable("a.b.c")); - assertFalse(HasNonNullVariableFunction.hasVariable("a.b.c.d")); - assertFalse(HasNonNullVariableFunction.hasVariable("")); - assertFalse(HasNonNullVariableFunction.hasVariable(null)); + assertTrue(HasNonNullVariableFunction.hasNonNullVariable("a")); + assertTrue(HasNonNullVariableFunction.hasNonNullVariable("a.b")); + assertTrue(HasNonNullVariableFunction.hasNonNullVariable("a.b.c")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("a.b.c.d")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable("")); + assertFalse(HasNonNullVariableFunction.hasNonNullVariable(null)); }); } diff --git a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ImmutablesTest.java b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ImmutablesTest.java index b9aa589899..b71e419eff 100644 --- a/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ImmutablesTest.java +++ b/runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ImmutablesTest.java @@ -44,7 +44,7 @@ public void test() throws Exception { .build(); EvalContextFactory ecf = new EvalContextFactoryImpl(); - ExpressionEvaluator ee = new DefaultExpressionEvaluator(new TaskProviders(), List.of(), List.of(), mock(SensitiveDataProcessor.class)); + ExpressionEvaluator ee = new DefaultExpressionEvaluator(new TaskProviders(), new FunctionHolder(), List.of(), List.of(), mock(SensitiveDataProcessor.class)); Map vars = Collections.singletonMap("testBean", testBean); // --- diff --git a/runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ELFunction.java b/runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ELFunction.java new file mode 100644 index 0000000000..b965ef6483 --- /dev/null +++ b/runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ELFunction.java @@ -0,0 +1,42 @@ +package com.walmartlabs.concord.runtime.v2.sdk; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * 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. + * ===== + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a static method as a function that can be called in JUEL expressions. + * The method must be static and will be registered with the provided name or + * using the method's name if omitted. + * Use ":" to separate the prefix and the local name of the function. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ELFunction { + + /** + * The name of the function to use in JUEL expressions. + */ + String value() default ""; +}