From 8204269f010aa25adf618c856aa2d3b0f1ace217 Mon Sep 17 00:00:00 2001
From: renczesstefan
+ * This class solves a critical memory optimization problem in the execution of process actions. Global functions
+ * defined in processes need to be accessible during action execution, but storing fully rehydrated closures
+ * for every function would consume significant heap space, especially in systems with many processes and functions.
+ *
+ * Instead of rehydrating all global functions for each action execution, this class acts as a lightweight proxy
+ * that:
+ *
+ *
+ *
+ * The typical usage pattern is: + *
+ * This design allows global functions to be shared across multiple action executions without rehydration, + * significantly reducing memory footprint and improving performance. + *
+ * + * @see ActionDelegate* @see com.netgrif.application.engine.workflow.service.interfaces.IFieldActionsCacheService* @see com.netgrif.application.engine.workflow.domain.CachedFunction + */ +class FunctionExpando extends Expando { + + /** + * Reference to the parent ActionDelegate that provides the execution context for actions. + *+ * This delegate is used to: + *
+ * By maintaining this reference, the FunctionExpando can act as a transparent proxy that adds + * function invocation capabilities while preserving all original ActionDelegate functionality. + *
+ */ + ActionDelegate parentDelegate + + /** + * Constructs a new FunctionExpando attached to the specified ActionDelegate. + *+ * The constructor establishes the delegation relationship that allows this FunctionExpando + * to act as a proxy for both function invocations and property access. + *
+ * + * @param parentDelegate the ActionDelegate that provides the execution context and handles + * non-function method calls and all property access + */ + FunctionExpando(ActionDelegate parentDelegate) { + this.parentDelegate = parentDelegate + } + + /** + * Intercepts method calls to provide dynamic resolution of function invocations. + *+ * This method implements Groovy's dynamic method resolution mechanism to handle calls + * that are not statically defined on this class. It follows this resolution strategy: + *
+ * This approach enables: + *
+ * This method handles dynamic property resolution for read operations using the following strategy: + *
+ * This two-tier approach ensures that: + *
+ * This method handles dynamic property resolution for write operations with special handling + * for Closure values (cached functions). The behavior is: + *
+ * This conditional storage approach ensures that: + *
+ * Note: Unlike the dual storage approach, Closure values are stored ONLY on this FunctionExpando instance, + * while non-Closure values are stored ONLY on the parent ActionDelegate, preventing unnecessary duplication + * and ensuring proper separation of concerns between function storage and data storage. + *
+ * + * @param name the name of the property being set + * @param value the value to assign to the property; Closures are stored locally, all other values are delegated + * @throws groovy.lang.MissingPropertyException if the property cannot be set on the parent delegate + */ + def propertyMissing(String name, value) { + if (value instanceof Closure) { + this."${name}" = value + } + parentDelegate."${name}" = value + } +} From fc5e1252e6993e37c0732ae5fd21642462a76bba Mon Sep 17 00:00:00 2001 From: renczesstefan+ * This class is designed to attach and execute process-scoped actions to {@link com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate} + * in a proper Groovy-like manner. It extends the standard {@link Expando} behavior by intercepting method calls + * and executing them as closures with proper delegation settings. + *
+ *+ * Unlike standard {@link Expando}, this implementation supports method overloading by storing multiple closures + * per method name in an internal function map. When a closure is set as a property, it is accumulated in a list + * associated with that property name, allowing multiple method signatures to coexist. + *
+ *+ * When a method is called that doesn't exist on this object, the {@link DelegateExpando#methodMissing(String, Object)} method + * is invoked, which looks up the method name in the function map and searches for a matching closure based on + * parameter count and type compatibility. If a matching closure is found, it is rehydrated with this instance as + * the delegate, owner, and thisObject, configured with {@link Closure#DELEGATE_FIRST} resolution strategy, and executed. + *
+ *+ * The closure rehydration process ensures that the closure executes in the context of this {@link DelegateExpando} + * instance, allowing it to access properties and methods defined on this object while maintaining proper delegation + * semantics for the ActionDelegate pattern. + *
+ *+ * Usage Example: + *
{@code
+ * def expando = new DelegateExpando()
+ * expando.greet = { String name -> "Hello, ${name}!" }
+ * expando.greet = { String name, String title -> "Hello, ${title} ${name}!" }
+ *
+ * expando.greet("John") // Returns "Hello, John!"
+ * expando.greet("Smith", "Dr.") // Returns "Hello, Dr. Smith!"
+ *}
+ *
+ * + * This class is primarily used by {@link FieldActionsRunner} to dynamically attach process-scoped functions + * to action delegates, enabling flexible and context-aware execution of workflow actions. + *
+ * + * @see Expando* @see com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate* @see FieldActionsRunner + */ +class DelegateExpando extends Expando { + + private static final String DO_CALL = "doCall" + + /** + * Internal storage for closures mapped by method name. + *+ * Each method name can map to multiple closures, enabling method overloading based on parameter types. + * The list maintains insertion order to preserve the sequence in which overloaded methods are defined. + *
+ */ + private final Map+ * This method provides access to the delegate object that will be set on cloned closures + * during {@link #methodMissing(String, Object)} execution. By default, it returns this + * instance, but can be overridden in subclasses to provide a different delegation target. + *
+ *+ * The returned delegate is used as the delegate, owner, and thisObject for all dynamically + * invoked closures, ensuring proper scope resolution within the ActionDelegate pattern. + *
+ * + * @return the delegate instance to be used for closure execution (this instance by default) + */ + DelegateExpando getDelegate() { + return this + } + + /** + * Overrides property setting to accumulate closures in the function map for method overloading support. + *+ * When a closure is assigned to a property, it is added to the list of closures associated with that + * property name in the function map, rather than replacing any existing closure. This accumulation + * mechanism is the foundation for method overloading: each closure represents a different method + * signature, and all are stored under the same method name. + *
+ *+ * The insertion order is preserved using a {@link LinkedHashMap}, ensuring that when multiple closures + * match during method resolution, the first matching closure (earliest defined) is selected. + *
+ *+ * Non-closure values are delegated to the parent {@link Expando} implementation, which stores them + * as regular dynamic properties accessible via standard property access syntax. + *
+ * + * @param name the property name; becomes the method name when value is a closure + * @param value the value to set; if a {@link Closure}, it is accumulated in the function map; + * otherwise handled by parent Expando + */ + void setProperty(String name, Object value) { + if (value instanceof Closure) { + functionMap.computeIfAbsent(name, { new ArrayList<>()}) << value + } else { + super.setProperty(name, value) + } + } + + /** + * Intercepts method calls that don't exist on this object and attempts to execute matching closures. + *+ * This method implements the dynamic method invocation and overload resolution mechanism by: + *
+ * Parameter matching is delegated to {@link ClosureSignatureMatcher#matches(Closure, Object [ ])}, which verifies + * that the argument count matches and each argument type is assignable to the corresponding parameter type. + * Null arguments are treated as compatible with any reference parameter type. The first matching closure in + * insertion order is selected and executed. + *
+ *+ * Closure Isolation: Each method invocation receives a clone of the matched closure, ensuring that + * modifications to delegate, resolveStrategy, or other closure properties don't affect subsequent calls. + *
+ * + * @param name the name of the method being called + * @param args the arguments passed to the method; can be an Object[], List, or single value + * @return the result of executing the matched closure + * @throws MissingMethodException if no closure with the given name exists or no closure matches the argument signature + */ + def methodMissing(String name, args) { + Object[] normalizedArgs = normalize(args) + + def candidates = functionMap[name] + if (candidates) { + Closure match = candidates.find { fn -> + ClosureSignatureMatcher.matches(fn, normalizedArgs) + } + if (match) { + def instance = match.clone() + ((Closure) instance).delegate = getDelegate() + ((Closure) instance).resolveStrategy = Closure.DELEGATE_FIRST + return ((Closure) instance).call(*normalizedArgs) + } + } + throw new MissingMethodException(name, this.class, args as Object[]) + } + + /** + * Normalizes method arguments into a consistent Object[] array format. + *+ * Groovy's method invocation can pass arguments in various forms depending on the call site. + * This method ensures that arguments are always converted to an Object[] array for consistent + * processing by {@link #methodMissing(String, Object)}. + *
+ *+ * Normalization rules: + *
+ * This class serves as a namespace container for global functions defined in business processes. It extends + * {@link DelegateExpando} to inherit property-based function storage capabilities while adding delegation + * to a parent {@link ActionDelegate} for non-function method calls. + *
+ *+ * Key characteristics: + *
+ * Typical usage in {@link FieldActionsRunner}: + *
+ * This design enables logical grouping of global functions while ensuring they have full access to the + * {@link ActionDelegate} execution environment. + *
+ * + * @see ActionDelegate* @see DelegateExpando* @see IFieldActionsCacheService* @see FieldActionsRunner + */ +class FunctionExpando extends DelegateExpando { + + /** + * Reference to the parent {@link ActionDelegate} that provides the execution context for actions. + *+ * This delegate is used for: + *
+ * Note: Property access is handled by the parent {@link DelegateExpando} class, not by direct delegation + * to this field. + *
+ */ + ActionDelegate parentDelegate + + /** + * Constructs a new {@code FunctionExpando} associated with the specified {@link ActionDelegate}. + *+ * The constructor establishes the delegation relationship that enables this {@code FunctionExpando} + * to fall back to the {@link ActionDelegate} for method calls that are not resolved as function closures. + *
+ * + * @param parentDelegate the {@link ActionDelegate} that provides the execution context for functions + * and handles non-function method calls + */ + FunctionExpando(ActionDelegate parentDelegate) { + this.parentDelegate = parentDelegate + } + + /** + * Returns the parent delegate that provides the execution context. + *+ * This method is overridden from {@link DelegateExpando} to return the {@link ActionDelegate} + * instance, enabling proper delegation and context provision for function closures. + *
+ * + * @return the parent {@link ActionDelegate} instance + */ + @Override + DelegateExpando getDelegate() { + return parentDelegate + } + + /** + * Intercepts method calls to provide two-level dynamic resolution: functions first, then {@link ActionDelegate}. + *+ * This method implements Groovy's dynamic method resolution mechanism to handle calls + * that are not statically defined on this class. It follows this resolution strategy: + *
+ * This two-level approach enables: + *
- * This class solves a critical memory optimization problem in the execution of process actions. Global functions - * defined in processes need to be accessible during action execution, but storing fully rehydrated closures - * for every function would consume significant heap space, especially in systems with many processes and functions. - *
- *- * Instead of rehydrating all global functions for each action execution, this class acts as a lightweight proxy - * that: - *
- * The typical usage pattern is: - *
- * This design allows global functions to be shared across multiple action executions without rehydration, - * significantly reducing memory footprint and improving performance. - *
- * - * @see ActionDelegate* @see com.netgrif.application.engine.workflow.service.interfaces.IFieldActionsCacheService* @see com.netgrif.application.engine.workflow.domain.CachedFunction - */ -class FunctionExpando extends Expando { - - /** - * Reference to the parent ActionDelegate that provides the execution context for actions. - *- * This delegate is used to: - *
- * By maintaining this reference, the FunctionExpando can act as a transparent proxy that adds - * function invocation capabilities while preserving all original ActionDelegate functionality. - *
- */ - ActionDelegate parentDelegate - - /** - * Constructs a new FunctionExpando attached to the specified ActionDelegate. - *- * The constructor establishes the delegation relationship that allows this FunctionExpando - * to act as a proxy for both function invocations and property access. - *
- * - * @param parentDelegate the ActionDelegate that provides the execution context and handles - * non-function method calls and all property access - */ - FunctionExpando(ActionDelegate parentDelegate) { - this.parentDelegate = parentDelegate - } - - /** - * Intercepts method calls to provide dynamic resolution of function invocations. - *- * This method implements Groovy's dynamic method resolution mechanism to handle calls - * that are not statically defined on this class. It follows this resolution strategy: - *
- * This approach enables: - *
- * This method handles dynamic property resolution for read operations using the following strategy: - *
- * This two-tier approach ensures that: - *
- * This method handles dynamic property resolution for write operations with special handling - * for Closure values (cached functions). The behavior is: - *
- * This conditional storage approach ensures that: - *
- * Note: Unlike the dual storage approach, Closure values are stored ONLY on this FunctionExpando instance, - * while non-Closure values are stored ONLY on the parent ActionDelegate, preventing unnecessary duplication - * and ensuring proper separation of concerns between function storage and data storage. - *
- * - * @param name the name of the property being set - * @param value the value to assign to the property; Closures are stored locally, all other values are delegated - * @throws groovy.lang.MissingPropertyException if the property cannot be set on the parent delegate - */ - def propertyMissing(String name, value) { - if (value instanceof Closure) { - this."${name}" = value - } - parentDelegate."${name}" = value - } -} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/utils/ClosureSignatureMatcher.java b/application-engine/src/main/java/com/netgrif/application/engine/utils/ClosureSignatureMatcher.java new file mode 100644 index 00000000000..1b065c904b0 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/utils/ClosureSignatureMatcher.java @@ -0,0 +1,59 @@ +package com.netgrif.application.engine.utils; + +import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.FieldActionsRunner; +import groovy.lang.Closure; + +import java.lang.reflect.Method; + +/** + * Utility class for matching Groovy Closure signatures against provided arguments. + *+ * This class provides functionality to determine whether a given Groovy {@link Closure} can accept + * a specific set of arguments based on its method signature. It inspects the closure's {@code doCall} + * methods and verifies if any of them match the provided argument types and count. + *
+ *+ * This is particularly useful when working with dynamic Groovy closures in a Java context, + * where compile-time type checking is not available and runtime signature matching is required. + *
+ * + * @see Closure + * @see FieldActionsRunner + */ +public class ClosureSignatureMatcher { + + /** + * Checks whether the provided Groovy closure can accept the given arguments. + *+ * This method iterates through all methods of the closure's class, looking for methods named {@code doCall}. + * For each {@code doCall} method found, it verifies: + *