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.