This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/4_0_X by this push:
     new fa206d31d6 [SYNCOPE-1927] Better documentation for Macro Tasks
fa206d31d6 is described below

commit fa206d31d6fde4fe20381749ef75cba20ae2edf9
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Fri Oct 31 14:16:18 2025 +0100

    [SYNCOPE-1927] Better documentation for Macro Tasks
---
 .../client/console/panels/DirectoryPanel.java      |   2 +-
 .../api/jexl/SyncopeJexlFunctions.java             |  27 ++-
 .../core/provisioning/api/jexl/JexlUtilsTest.java  |  16 +-
 pom.xml                                            |   2 +-
 .../concepts/externalresources.adoc                |   8 +-
 .../reference-guide/concepts/notifications.adoc    |   7 +-
 .../asciidoc/reference-guide/concepts/tasks.adoc   | 227 ++++++++++++++++++++-
 .../reference-guide/concepts/typemanagement.adoc   |  10 +-
 src/main/asciidoc/reference-guide/usage/core.adoc  |  18 ++
 .../reference-guide/usage/customization.adoc       |   4 -
 10 files changed, 294 insertions(+), 27 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DirectoryPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DirectoryPanel.java
index f5fa77fbf0..95fdfa2e5b 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DirectoryPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DirectoryPanel.java
@@ -191,7 +191,7 @@ public abstract class DirectoryPanel<
 
             send(DirectoryPanel.this, Broadcast.EXACT, data);
 
-            modal.show(false);
+            displayAttributeModal.show(false);
         });
     }
 
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
index 815f84cd3a..2aa7f0ff8a 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
@@ -19,7 +19,7 @@
 package org.apache.syncope.core.provisioning.api.jexl;
 
 import java.util.Arrays;
-import java.util.Collections;
+import java.util.Base64;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -63,8 +63,7 @@ public class SyncopeJexlFunctions {
         }
 
         List<String> headless = Arrays.asList(fullPathSplitted).subList(1, 
fullPathSplitted.length);
-        Collections.reverse(headless);
-        return prefix + attr + "=" + 
headless.stream().collect(Collectors.joining("," + attr + "="));
+        return prefix + attr + "=" + 
headless.reversed().stream().collect(Collectors.joining("," + attr + "="));
     }
 
     /**
@@ -78,6 +77,26 @@ public class SyncopeJexlFunctions {
         return Optional.ofNullable(connObj).
                 flatMap(obj -> 
Optional.ofNullable(obj.getAttributeByName(name)).
                 map(Attribute::getValue)).
-            orElseGet(List::of);
+                orElseGet(List::of);
+    }
+
+    /**
+     * Encodes the given byte array as Base64-encoded string.
+     *
+     * @param value byte array value
+     * @return
+     */
+    public String base64Encode(final byte[] value) {
+        return Base64.getEncoder().encodeToString(value);
+    }
+
+    /**
+     * Decodes the given string as byte array using Base64 encoding.
+     *
+     * @param value base64 string
+     * @return
+     */
+    public byte[] base64Decode(final String value) {
+        return Base64.getDecoder().decode(value);
     }
 }
diff --git 
a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
 
b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
index c954027773..ab943900d9 100644
--- 
a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
+++ 
b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
@@ -29,10 +29,12 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.AnyTO;
@@ -61,7 +63,7 @@ public class JexlUtilsTest extends AbstractTest {
     }
 
     @Test
-    public void evaluate() {
+    public void evaluateExpr() {
         String expression = null;
         assertEquals(StringUtils.EMPTY, JexlUtils.evaluateExpr(expression, 
context));
 
@@ -161,4 +163,16 @@ public class JexlUtilsTest extends AbstractTest {
         assertTrue(JexlUtils.evaluateMandatoryCondition("true", any, 
derAttrHandler));
         assertFalse(JexlUtils.evaluateMandatoryCondition("false", any, 
derAttrHandler));
     }
+
+    @Test
+    public void evaluateTemplate() {
+        byte[] byteArray = "a value".getBytes();
+        String result = JexlUtils.evaluateTemplate(
+                "${syncope:base64Encode(value)}", new 
MapContext(Map.of("value", byteArray)));
+        assertEquals(Base64.getEncoder().encodeToString(byteArray), result);
+
+        result = JexlUtils.evaluateTemplate(
+                "${syncope:fullPath2Dn(value, 'ou')}", new 
MapContext(Map.of("value", "/a/b/c")));
+        assertEquals("ou=c,ou=b,ou=a", result);
+    }
 }
diff --git a/pom.xml b/pom.xml
index 5002ec9587..49cc1c1ba3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -430,7 +430,7 @@ under the License.
     <disruptor.version>4.0.0</disruptor.version>
 
     <elasticsearch.version>9.2.0</elasticsearch.version>
-    <opensearch.version>3.3.1</opensearch.version>
+    <opensearch.version>3.3.2</opensearch.version>
     <opensearch-java.version>3.3.0</opensearch-java.version>
 
     <openfga.version>v1</openfga.version>
diff --git a/src/main/asciidoc/reference-guide/concepts/externalresources.adoc 
b/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
index 240ce60ccf..5bf40a0d88 100644
--- a/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
@@ -136,7 +136,7 @@ owned by the any object of type `relationshipAnyType`, if a 
relationship of type
 ** `memberships[groupName].schema` - resolves to the attribute for the given 
`schema`, owned by the membership for group
 `groupName` of the mapped entity (user, any object), if such a membership 
exists
 * external attribute - the name of the attribute on the Identity Store
-* transformers - http://commons.apache.org/proper/commons-jexl/[JEXL^] 
expression or Java class implementing
+* transformers - <<jexl,JEXL>> expression or Java class implementing
 ifeval::["{snapshotOrRelease}" == "release"]
 
https://github.com/apache/syncope/blob/syncope-{docVersion}/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ItemTransformer.java[ItemTransformer^]
 endif::[]
@@ -144,9 +144,9 @@ ifeval::["{snapshotOrRelease}" == "snapshot"]
 
https://github.com/apache/syncope/blob/4_0_X/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ItemTransformer.java[ItemTransformer^]
 endif::[]
 ; the purpose is to transform values before they are sent to or received from 
the underlying connector
-* mandatory condition - http://commons.apache.org/proper/commons-jexl/[JEXL^] 
expression indicating whether values for 
-this mapping item must be necessarily available or not; compared to a simple 
boolean value, such condition allows
-complex statements to be expressed such as 'be mandatory only if this other 
attribute value is above 14', and so on
+* mandatory condition - <<jexl,JEXL>> expression indicating whether values for 
this mapping item must be necessarily
+available or not; compared to a simple boolean value, such condition allows 
complex statements to be expressed such as
+'be mandatory only if this other attribute value is above 14', and so on
 * remote key flag - should this item be considered as the key value on the 
Identity Store, if no
 <<inbound-correlation-rules,inbound>> or <<push-correlation-rules,push>> 
correlation rules are applicable?
 * password flag (Users only) - should this item be treated as the password 
value?
diff --git a/src/main/asciidoc/reference-guide/concepts/notifications.adoc 
b/src/main/asciidoc/reference-guide/concepts/notifications.adoc
index 7a82276a76..745fbac0db 100644
--- a/src/main/asciidoc/reference-guide/concepts/notifications.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/notifications.adoc
@@ -101,16 +101,15 @@ other custom code.
 
 ==== Notification Templates
 
-A notification template is defined as a pair of 
http://commons.apache.org/proper/commons-jexl/[JEXL^] expressions,
-to be used respectively for plaintext and HTML e-mails, and is available for 
selection in the notification specification.
+A notification template is defined as a pair of <<jexl,JEXL>> expressions, to 
be used respectively for plaintext and
+HTML e-mails, and is available for selection in the notification specification.
 
 [NOTE]
 ====
 Notification templates can be easily managed via the 
<<console-configuration-notifications,admin console>>.
 ====
 
-The full power of JEXL expressions - see 
http://commons.apache.org/proper/commons-jexl/reference/syntax.html[reference^]
-and http://commons.apache.org/proper/commons-jexl/reference/examples.html[some 
examples^] - is available. +
+The full power of JEXL expressions is available. +
 For example, the `user` variable, an instance of 
 ifeval::["{snapshotOrRelease}" == "release"]
 
https://github.com/apache/syncope/blob/syncope-{docVersion}/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/UserTO.java[UserTO^]
diff --git a/src/main/asciidoc/reference-guide/concepts/tasks.adoc 
b/src/main/asciidoc/reference-guide/concepts/tasks.adoc
index aee40faa07..c7f1b9e93c 100644
--- a/src/main/asciidoc/reference-guide/concepts/tasks.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/tasks.adoc
@@ -249,11 +249,11 @@ and are permanently saved - for later re-execution or for 
examining the executio
 ==== Macros
 
 Macro tasks are meant to group one or more <<commands>> into a given execution 
sequence, alongside with
-arguments required to run.
+arguments required to run, with option to define an input form to drive user 
interaction.
 
 When defining a macro task, the following information must be provided:
 
-* commands to run, with their args
+* commands to run with their args, either statically defined or mapped to form 
properties by <<jexl,JEXL>> expressions
 * <<realms,Realm>> for <<delegated-administration,delegated administration>> 
to restrict the set of users entitled to
 list, update or execute the given macro task
 * scheduling information:
@@ -263,7 +263,7 @@ list, update or execute the given macro task
 ===== MacroActions
 
 Macro task execution can be decorated with custom logic to be invoked around 
task execution, by associating
-macro tasks to one or more <<implementations,implementations>> of the
+macro tasks to a given <<implementations,Implementation>> of the
 ifeval::["{snapshotOrRelease}" == "release"]
 
https://github.com/apache/syncope/blob/syncope-{docVersion}/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/MacroActions.java[MacroActions^]
 endif::[]
@@ -272,6 +272,227 @@ 
https://github.com/apache/syncope/blob/4_0_X/core/idrepo/logic/src/main/java/org
 endif::[]
 interface.
 
+.Macro task with input form
+====
+Let's assume there are two `Command` instances defined, alongside with their 
arguments:
+
+. `NewPrinterCommand` which creates a new Realm and an AnyObject instance of 
type `PRINTER` beloging to it
+. `BinaryCommand` which simply logs about the received argument
+
+[source,java]
+----
+public class NewPrinterCommandArgs extends CommandArgs {
+
+    @NotEmpty
+    @Schema(description = "parent realm", example = "/even/two", defaultValue 
= "/",
+            requiredMode = Schema.RequiredMode.REQUIRED)
+    private String parentRealm = "/";
+
+    @NotEmpty
+    @Schema(description = "new realm name", example = "realm123", 
+            requiredMode = Schema.RequiredMode.REQUIRED)
+    private String realmName;
+
+    @NotEmpty
+    @Schema(description = "printer name", example = "printer123", 
+            requiredMode = Schema.RequiredMode.REQUIRED)
+    private String printerName;
+
+    // getter and setter methods omitted
+}
+----
+
+[source,java]
+----
+public class NewPrinterCommand implements Command<NewPrinterCommandArgs> {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(NewPrinterCommand.class);
+
+    @Autowired
+    private RealmLogic realmLogic;
+
+    @Autowired
+    private AnyObjectLogic anyObjectLogic;
+
+    private Optional<RealmTO> getRealm(final String fullPath) {
+        return realmLogic.search(null, Set.of(fullPath), 
Pageable.unpaged()).get().
+                filter(realm -> 
fullPath.equals(realm.getFullPath())).findFirst();
+    }
+
+    @Transactional(propagation = Propagation.NOT_SUPPORTED)
+    @Override
+    public Result run(final TestCommandArgs args) {
+        // 1. create new Realm
+        RealmTO realm = new RealmTO();
+        realm.setName(args.getRealmName());
+        
realm.setParent(getRealm(args.getParentRealm()).map(RealmTO::getKey).orElse(null));
+        realm = realmLogic.create(args.getParentRealm(), realm).getEntity();
+        LOG.info("Realm created: {}", realm.getFullPath());
+
+        // 2. create new PRINTER
+        AnyObjectTO anyObject = anyObjectLogic.create(new AnyObjectCR.Builder(
+                        realm.getFullPath(), "PRINTER", args.getPrinterName()).
+                        plainAttr(new 
Attr.Builder("location").value("location").build()).
+                        build(),
+                false).getEntity();
+        LOG.info("PRINTER created: {}", anyObject.getName());
+
+        return new Result(
+                "Realm created: " + realm.getFullPath() 
+                + "; PRINTER created: " + anyObject.getName(),
+                Map.of("realm", realm.getKey(), "PRINTER", 
anyObject.getKey()));
+    }
+}
+----
+
+[source,java]
+----
+public class BinaryCommandArgs extends CommandArgs {
+
+    private static final long serialVersionUID = -8257974017887359696L;
+
+    private String binaryParam;
+
+    // getter and setter method omitted
+}
+----
+
+[source,java]
+----
+public class BinaryCommand implements Command<BinaryCommandArgs> {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(BinaryCommand.class);
+
+    private static final Tika TIKA = new Tika();
+
+    static {
+        TIKA.setMaxStringLength(-1);
+    }
+
+    @Override
+    public Result run(final BinaryCommandArgs args) {
+        String base64 = args.getBinaryParam();
+        LOG.info("Input value received: {}", base64);
+
+        byte[] binaryValue = Base64.getDecoder().decode(base64);
+        LOG.info("Byte array with length {} and mime type {}", 
+                 binaryValue.length, TIKA.detect(binaryValue));
+
+        return new Result("SUCCESS", Map.of());
+    }
+}
+----
+
+Let's also assume that the following `MacroActions` instance is available, 
showing how to populate dropdown values,
+how to perform some example validation and also allowing to alter the 
arguments value before the related command is
+executed:
+
+[source,java]
+----
+public class SampleMacroActions implements MacroActions {
+
+    @Autowired
+    private RealmDAO realmDAO;
+
+    @Autowired
+    private RealmSearchDAO realmSearchDAO;
+
+    @Transactional(readOnly = true)
+    @Override
+    public Map<String, String> getDropdownValues(final String formProperty) {
+        return realmSearchDAO.findChildren(realmDAO.getRoot()).stream().
+                collect(Collectors.toMap(Realm::getFullPath, Realm::getName));
+    }
+
+    @Override
+    public void validate(final SyncopeForm form, final Map<String, Object> 
vars) 
+        throws ValidationException {
+
+        Object binaryValue = vars.get("binaryField");
+        if (!(binaryValue instanceof byte[])) {
+            throw new ValidationException(
+              "Expected byte[], found " + binaryValue.getClass().getName());
+        }
+    }
+
+    @Override
+    public void beforeCommand(final Command<CommandArgs> command, final 
CommandArgs args) {
+        if (args instanceof BinaryCommandArgs binaryCommandArgs) {
+            // option to alter binaryCommandArgs before the related command is 
executed
+        }
+    }
+}
+----
+
+It is now possible to define the following Macro task (details are simplified 
to increase readability):
+
+[source,json]
+----
+    {
+      "key": "019a35af-1a5a-75d1-920c-7f388696be0d",
+      "cronExpression": null,
+      "jobDelegate": "MacroJobDelegate",
+      "name": "BinaryMacro",
+      "realm": "/",
+      "formPropertyDefs": [
+        {
+          "name": "parent",  // <1>
+          "type": "Dropdown",
+          "readable": true,
+          "writable": true,
+          "required": true
+        },
+        {
+          "name": "realm", // <2>
+          "type": "String",
+          "readable": true,
+          "writable": true,
+          "required": true
+        },
+        {
+          "name": "binaryField", // <3>
+          "type": "Binary",
+          "readable": true,
+          "writable": true,
+          "required": true,
+          "mimeType": "application/pdf"
+        }
+      ],
+      "commands": [
+        {
+          "key": "NewPrinterCommand",
+          "args": {
+            "_class": 
"org.apache.syncope.common.lib.command.NewPrinterCommandArgs",
+            "parentRealm": "${parent}",  // <4>
+            "realmName": "${realm}"  // <5>
+          }
+        },
+        {
+          "key": "BinaryCommand",
+          "args": {
+            "_class": 
"org.apache.syncope.common.lib.command.BinaryCommandArgs",
+            "binaryParam": "${syncope:base64Encode(binaryField)}"  // <6>
+          }
+        }
+      ],
+      "continueOnError": false,
+      "saveExecs": true,
+      "macroActions": "SampleMacroActions"
+    }
+----
+<1> dropdown form property whose values are generated by 
`SampleMacroActions#getDropdownValues`
+<2> string form property
+<3> binary form property expecting a PDF input file
+<4> binds the `parentRealm` property of `NewPrinterCommandArgs` to the value 
provided for the form property `parent`
+<5> binds the `realmName` property of `NewPrinterCommandArgs` to the value 
provided for the form property `realm`
+<6> binds the `binaryParam` property of `BinaryCommandArgs` to the 
Base64-decoded value provided for the
+form property `binaryField`
+
+[TIP]
+Please take into account that both defining and running a Macro task form are 
a much pleasant experience when
+performed in the <<Admin Console>>.
+====
+
 [[tasks-scheduled]]
 ==== Scheduled
 
diff --git a/src/main/asciidoc/reference-guide/concepts/typemanagement.adoc 
b/src/main/asciidoc/reference-guide/concepts/typemanagement.adoc
index a449ec35e9..01d68adfa2 100644
--- a/src/main/asciidoc/reference-guide/concepts/typemanagement.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/typemanagement.adoc
@@ -60,9 +60,9 @@ ifeval::["{snapshotOrRelease}" == "snapshot"]
 
https://github.com/apache/syncope/blob/4_0_X/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/attrvalue/EmailAddressValidator.java[EmailAddressValidator^]
 endif::[]
 for reference
-* Mandatory condition - http://commons.apache.org/proper/commons-jexl/[JEXL^] 
expression indicating whether values for 
-this schema must be necessarily provided or not; compared to simple boolean 
value, such condition allows to express
-complex statements like 'be mandatory only if this other attribute value is 
above 14', and so on
+* Mandatory condition - <<jexl,JEXL>> expression indicating whether values for 
 this schema must be necessarily provided
+or not; compared to simple boolean value, such condition allows to express 
complex statements like 'be mandatory only if
+this other attribute value is above 14', and so on
 * Unique constraint - make sure that no duplicate value(s) for this schema are 
found
 * Multivalue flag - whether single or multiple values are supported
 * Read-only flag - whether value(s) for this schema are modifiable only via 
internal code (say workflow tasks) or 
@@ -74,8 +74,8 @@ Sometimes it is useful to obtain values as arbitrary 
combinations of other attri
 `firstname` and `surname` plain schemas, it is natural to think that 
`fullname` could be somehow defined as the 
 concatenation of `firstname` 's and `surname` 's values, separated by a blank 
space.
 
-Derived schemas are always read-only and require a 
http://commons.apache.org/proper/commons-jexl/[JEXL^]
-expression to be specified that references plain schema types. +
+Derived schemas are always read-only and require a <<jexl,JEXL>> expression to 
be specified that references plain schema
+types. +
 For the sample above, it would be
 
  firstname + ' ' + surname
diff --git a/src/main/asciidoc/reference-guide/usage/core.adoc 
b/src/main/asciidoc/reference-guide/usage/core.adoc
index 997a6f3db5..92c4c7f2d3 100644
--- a/src/main/asciidoc/reference-guide/usage/core.adoc
+++ b/src/main/asciidoc/reference-guide/usage/core.adoc
@@ -543,3 +543,21 @@ username DESC
 email DESC, username ASC
 ----
 ====
+
+[[jexl]]
+==== JEXL support
+
+https://commons.apache.org/proper/commons-jexl/[Apache Commons JEXL^] is 
supported as a mean to implement templating
+and dynamic value calculation.
+
+Besides 
https://commons.apache.org/proper/commons-jexl/reference/syntax.html[standard 
syntax^], the following additional
+functions are defined:
+
+* `syncope:fullPath2Dn(fullPath, attr)` - converts full path into the 
equivalent DN; for example, `/a/b/c` becomes
+`ou=c,ou=b,ou=a`
+* `syncope:fullPath2Dn(fullPath, attr, prefix)` - converts full path into the 
equivalent DN, with prefix;
+for example, `/a/b/c` with prefix `o=isp,` becomes `o=isp,ou=c,ou=b,ou=a`
+* `syncope:connObjAttrValues(connObj, name)` - extracts the values of the 
attribute with given name from the given
+connector object, or empty list if not found
+* `syncope:base64Encode(value)` - encodes the given byte array as 
Base64-encoded string
+* `syncope:base64Decode(value)` - decodes the given string as byte array using 
Base64 encoding
diff --git a/src/main/asciidoc/reference-guide/usage/customization.adoc 
b/src/main/asciidoc/reference-guide/usage/customization.adoc
index 4a7013ceb5..413d8a91b5 100644
--- a/src/main/asciidoc/reference-guide/usage/customization.adoc
+++ b/src/main/asciidoc/reference-guide/usage/customization.adoc
@@ -710,7 +710,3 @@ available at runtime.
 ==== Extensions
 
 <<extensions>> can be part of a local project, to encapsulate special features 
which are specific to a given deployment.
-
-For example, the http://www.chorevolution.eu/[CHOReVOLUTION^] IdM - based on 
Apache Syncope - provides
-https://gitlab.ow2.org/chorevolution/syncope/tree/4_0_X/ext/choreography[an 
extension^]
-for managing via the <<core>> and visualizing via the 
<<admin-console-component>> the running choreography instances.

Reply via email to