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

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

commit 58f9e1ae831a330096ff6eedd07bf7f0c04213dd
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Wed May 15 16:06:03 2024 +0200

    [SYNCOPE-1815] Further Macro improvements (#713)
---
 ...IndicatorAjaxFormComponentUpdatingBehavior.java |   2 +-
 .../markup/html/form/AjaxPasswordFieldPanel.java   |   5 +
 .../client/ui/commons/panels/SyncopeFormPanel.java | 102 ++++++++------
 .../client/ui/commons/panels/SyncopeFormPanel.html |   2 +-
 .../client/console/SyncopeConsoleSession.java      |   2 +-
 .../syncope/client/console/pages/Realms.java       |   3 +-
 .../console/tasks/FormPropertyDefsPanel.java       | 148 ++++++++++++++++++---
 .../console/tasks/MacroTaskDirectoryPanel.java     |   2 +
 .../console/wizards/any/ConsoleAuxClasses.java     |   2 +-
 .../client/console/wizards/any/Details.java        |  18 +--
 .../console/implementations/MyMacroActions.groovy  |  18 ++-
 .../console/tasks/FormPropertyDefsPanel.html       |  18 +--
 .../console/tasks/FormPropertyDefsPanel.properties |   8 +-
 .../tasks/FormPropertyDefsPanel_fr_CA.properties   |   8 +-
 .../tasks/FormPropertyDefsPanel_it.properties      |   8 +-
 .../tasks/FormPropertyDefsPanel_ja.properties      |   8 +-
 .../tasks/FormPropertyDefsPanel_pt_BR.properties   |   8 +-
 .../tasks/FormPropertyDefsPanel_ru.properties      |   8 +-
 .../markup/html/form/ActionsPanel.properties       |   6 +
 .../markup/html/form/ActionsPanel_fr_CA.properties |   6 +
 .../markup/html/form/ActionsPanel_it.properties    |   6 +
 .../markup/html/form/ActionsPanel_ja.properties    |   6 +
 .../markup/html/form/ActionsPanel_pt_BR.properties |   6 +
 .../markup/html/form/ActionsPanel_ru.properties    |   6 +
 .../syncope/common/lib/form/FormProperty.java      |  37 ++++++
 .../syncope/common/lib/to/FormPropertyDefTO.java   |  37 ++++++
 .../org/apache/syncope/core/logic/TaskLogic.java   |   3 +-
 .../api/entity/task/FormPropertyDef.java           |  13 ++
 .../jpa/entity/task/JPAFormPropertyDef.java        |  52 +++++++-
 .../persistence/jpa/entity/task/JPAMacroTask.java  |   9 +-
 .../jpa/entity/task/JPAMacroTaskCommand.java       |   6 +
 .../neo4j/entity/task/Neo4jFormPropertyDef.java    |  46 ++++++-
 .../task/Neo4jFormPropertyDefRelationship.java     | 102 ++++++++++++++
 .../neo4j/entity/task/Neo4jMacroTask.java          |  11 +-
 .../core/provisioning/api/macro/MacroActions.java  |   9 +-
 .../java/data/ImplementationDataBinderImpl.java    |  10 ++
 .../provisioning/java/data/TaskDataBinderImpl.java |  15 ++-
 .../provisioning/java/job/MacroJobDelegate.java    | 137 +++++++++++--------
 .../console/panels/UserRequestFormPanel.java       |   4 +-
 39 files changed, 728 insertions(+), 169 deletions(-)

diff --git 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/ajax/form/IndicatorAjaxFormComponentUpdatingBehavior.java
 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/ajax/form/IndicatorAjaxFormComponentUpdatingBehavior.java
index 93b58565d6..b38e1ea7f6 100644
--- 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/ajax/form/IndicatorAjaxFormComponentUpdatingBehavior.java
+++ 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/ajax/form/IndicatorAjaxFormComponentUpdatingBehavior.java
@@ -23,7 +23,7 @@ import org.apache.wicket.ajax.IAjaxIndicatorAware;
 import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
 
 /**
- * An {@link AjaxFormComponentUpdatingBehavior} not showin veil.
+ * An {@link AjaxFormComponentUpdatingBehavior} not showing veil.
  */
 public abstract class IndicatorAjaxFormComponentUpdatingBehavior
         extends AjaxFormComponentUpdatingBehavior implements 
IAjaxIndicatorAware {
diff --git 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPasswordFieldPanel.java
 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPasswordFieldPanel.java
index 6253f5f50a..da0a64337e 100644
--- 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPasswordFieldPanel.java
+++ 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPasswordFieldPanel.java
@@ -66,6 +66,11 @@ public class AjaxPasswordFieldPanel extends 
FieldPanel<String> {
         }
     }
 
+    public AjaxPasswordFieldPanel setResetPassword(final boolean 
resetPassword) {
+        ((PasswordTextField) field).setResetPassword(resetPassword);
+        return this;
+    }
+
     @Override
     public FieldPanel<String> addRequiredLabel() {
         if (!isRequired()) {
diff --git 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.java
 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.java
index 2cfc3cb4fc..efc0e32371 100644
--- 
a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.java
+++ 
b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.java
@@ -19,19 +19,24 @@
 package org.apache.syncope.client.ui.commons.panels;
 
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.commons.lang3.time.FastDateFormat;
 import org.apache.syncope.client.ui.commons.MapChoiceRenderer;
+import 
org.apache.syncope.client.ui.commons.markup.html.form.AbstractFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
-import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
 import org.apache.syncope.common.lib.form.FormProperty;
 import org.apache.syncope.common.lib.form.FormPropertyValue;
 import org.apache.syncope.common.lib.form.SyncopeForm;
@@ -39,8 +44,9 @@ import org.apache.wicket.markup.html.list.ListItem;
 import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.LoadableDetachableModel;
 import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.model.util.ListModel;
+import org.apache.wicket.validation.validator.PatternValidator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,47 +59,36 @@ public class SyncopeFormPanel<F extends SyncopeForm> 
extends Panel {
     public SyncopeFormPanel(final String id, final F form) {
         super(id);
 
-        IModel<List<FormProperty>> formProps = new LoadableDetachableModel<>() 
{
+        ListModel<FormProperty> model = new ListModel<>(new ArrayList<>());
+        model.getObject().addAll(form.getProperties());
 
-            private static final long serialVersionUID = 3169142472626817508L;
-
-            @Override
-            protected List<FormProperty> load() {
-                return form.getProperties();
-            }
-        };
-
-        ListView<FormProperty> propView = new ListView<>("propView", 
formProps) {
+        ListView<FormProperty> propView = new ListView<>("propView", model) {
 
             private static final long serialVersionUID = 9101744072914090143L;
 
             @Override
-            @SuppressWarnings({ "unchecked", "rawtypes" })
             protected void populateItem(final ListItem<FormProperty> item) {
                 FormProperty prop = item.getModelObject();
 
                 String label = StringUtils.isBlank(prop.getName()) ? 
prop.getId() : prop.getName();
 
-                FieldPanel field;
+                AbstractFieldPanel<?> field;
                 switch (prop.getType()) {
                     case Boolean:
-                        field = new AjaxDropDownChoicePanel("value", label, 
new PropertyModel<String>(prop, "value") {
+                        field = new AjaxCheckBoxPanel("value", label, new 
PropertyModel<Boolean>(prop, "value") {
 
                             private static final long serialVersionUID = 
-3743432456095828573L;
 
                             @Override
-                            public String getObject() {
-                                return StringUtils.isBlank(prop.getValue())
-                                        ? null
-                                        : prop.getValue().equals("true") ? 
"Yes" : "No";
+                            public Boolean getObject() {
+                                return BooleanUtils.toBoolean(prop.getValue());
                             }
 
                             @Override
-                            public void setObject(final String object) {
-                                
prop.setValue(String.valueOf(object.equalsIgnoreCase("yes")));
+                            public void setObject(final Boolean object) {
+                                
prop.setValue(BooleanUtils.toStringTrueFalse(object));
                             }
-
-                        }, false).setChoices(List.of("Yes", "No"));
+                        }, false);
                         break;
 
                     case Date:
@@ -122,25 +117,56 @@ public class SyncopeFormPanel<F extends SyncopeForm> 
extends Panel {
                         break;
 
                     case Enum:
-                        field = new AjaxDropDownChoicePanel(
+                        field = new AjaxDropDownChoicePanel<>(
                                 "value", label, new 
PropertyModel<String>(prop, "value"), false).
                                 setChoiceRenderer(new 
MapChoiceRenderer(prop.getEnumValues().stream().
                                         collect(Collectors.toMap(
                                                 FormPropertyValue::getKey,
                                                 
FormPropertyValue::getValue)))).
-                                setChoices(prop.getEnumValues().stream().
-                                        
map(FormPropertyValue::getKey).collect(Collectors.toList()));
+                                
setChoices(prop.getEnumValues().stream().map(FormPropertyValue::getKey).toList());
                         break;
 
                     case Dropdown:
-                        field = new AjaxDropDownChoicePanel(
-                                "value", label, new 
PropertyModel<String>(prop, "value"), false).
-                                setChoiceRenderer(new 
MapChoiceRenderer(prop.getDropdownValues().stream().
-                                        collect(Collectors.toMap(
-                                                FormPropertyValue::getKey,
-                                                
FormPropertyValue::getValue)))).
-                                setChoices(prop.getDropdownValues().stream().
-                                        
map(FormPropertyValue::getKey).collect(Collectors.toList()));
+                        if (prop.isDropdownFreeForm()) {
+                            field = new AjaxTextFieldPanel("value", label, new 
PropertyModel<>(prop, "value"), false);
+                            ((AjaxTextFieldPanel) 
field).setChoices(prop.getDropdownValues().stream().
+                                    map(FormPropertyValue::getKey).toList());
+                        } else if (prop.isDropdownSingleSelection()) {
+                            field = new AjaxDropDownChoicePanel<>(
+                                    "value", label, new 
PropertyModel<String>(prop, "value"), false).
+                                    setChoiceRenderer(new 
MapChoiceRenderer(prop.getDropdownValues().stream().
+                                            collect(Collectors.toMap(
+                                                    FormPropertyValue::getKey,
+                                                    
FormPropertyValue::getValue)))).
+                                    
setChoices(prop.getDropdownValues().stream().
+                                            
map(FormPropertyValue::getKey).toList());
+                        } else {
+                            field = new 
AjaxPalettePanel.Builder<String>().setName(label).
+                                    setRenderer(new 
MapChoiceRenderer(prop.getDropdownValues().stream().
+                                            collect(Collectors.toMap(
+                                                    FormPropertyValue::getKey,
+                                                    
FormPropertyValue::getValue)))).build(
+                                    "value",
+                                    new IModel<List<String>>() {
+
+                                private static final long serialVersionUID = 
1015030402166681242L;
+
+                                @Override
+                                public List<String> getObject() {
+                                    return 
Optional.ofNullable(prop.getValue()).
+                                            map(v -> List.of(v.split(";"))).
+                                            orElse(null);
+                                }
+
+                                @Override
+                                public void setObject(final List<String> 
object) {
+                                    prop.setValue(Optional.ofNullable(object).
+                                            map(v -> 
v.stream().collect(Collectors.joining(";"))).
+                                            orElse(null));
+                                }
+                            }, new 
ListModel<>(prop.getDropdownValues().stream().
+                                            
map(FormPropertyValue::getKey).toList()));
+                        }
                         break;
 
                     case Long:
@@ -167,12 +193,15 @@ public class SyncopeFormPanel<F extends SyncopeForm> 
extends Panel {
                         break;
 
                     case Password:
-                        field = new AjaxPasswordFieldPanel("value", label, new 
PropertyModel<>(prop, "value"), false);
+                        field = new AjaxPasswordFieldPanel("value", label, new 
PropertyModel<>(prop, "value"), false).
+                                setResetPassword(false);
                         break;
 
                     case String:
                     default:
                         field = new AjaxTextFieldPanel("value", label, new 
PropertyModel<>(prop, "value"), false);
+                        Optional.ofNullable(prop.getStringRegEx()).
+                                ifPresent(re -> ((AjaxTextFieldPanel) 
field).addValidator(new PatternValidator(re)));
                         break;
                 }
 
@@ -184,7 +213,6 @@ public class SyncopeFormPanel<F extends SyncopeForm> 
extends Panel {
                 item.add(field);
             }
         };
-
-        add(propView);
+        add(propView.setReuseItems(true));
     }
 }
diff --git 
a/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.html
 
b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.html
index 127037bc48..6271a58363 100644
--- 
a/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.html
+++ 
b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/SyncopeFormPanel.html
@@ -22,6 +22,6 @@ under the License.
       <span wicket:id="value">[value]</span>
     </div>
 
-    <wicket:child/>    
+    <wicket:child/>
   </wicket:panel>
 </html>
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
index 153a4d4118..543089041b 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
@@ -162,7 +162,7 @@ public class SyncopeConsoleSession extends 
AuthenticatedWebSession implements Ba
 
         message = getApplication().getResourceSettings().getLocalizer().
                 getString(message, null, null, null, null, message);
-        error(message);
+        error(message.replace("\n", "<br/>"));
     }
 
     public MediaType getMediaType() {
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Realms.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Realms.java
index 47ebc8bea4..8f52b00750 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Realms.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Realms.java
@@ -257,8 +257,7 @@ public class Realms extends BasePage {
                 target.add(content);
             } catch (Exception e) {
                 LOG.error("While deleting realm", e);
-                // Escape line breaks
-                SyncopeConsoleSession.get().error(e.getMessage().replace("\n", 
" "));
+                SyncopeConsoleSession.get().onException(e);
             }
             ((BaseWebPage) 
Realms.this.getPage()).getNotificationPanel().refresh(target);
         }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.java
index 3d6210f97f..8b59be9fb3 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.java
@@ -21,6 +21,9 @@ package org.apache.syncope.client.console.tasks;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.panels.AbstractModalPanel;
@@ -49,6 +52,9 @@ import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.PropertyModel;
 import org.apache.wicket.model.util.ListModel;
 import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.apache.wicket.validation.IValidatable;
+import org.apache.wicket.validation.IValidator;
+import org.apache.wicket.validation.ValidationError;
 
 public class FormPropertyDefsPanel extends AbstractModalPanel<MacroTaskTO> {
 
@@ -70,8 +76,7 @@ public class FormPropertyDefsPanel extends 
AbstractModalPanel<MacroTaskTO> {
         this.task = task;
 
         WebMarkupContainer propertyDefContainer = new 
WebMarkupContainer("propertyDefContainer");
-        propertyDefContainer.setOutputMarkupId(true);
-        add(propertyDefContainer);
+        add(propertyDefContainer.setOutputMarkupId(true));
 
         model = new ListModel<>(new ArrayList<>());
         model.getObject().addAll(task.getFormPropertyDefs());
@@ -127,20 +132,69 @@ public class FormPropertyDefsPanel extends 
AbstractModalPanel<MacroTaskTO> {
                 
type.setChoices(List.of(FormPropertyType.values())).setNullValid(false);
                 item.add(type.setRequired(true).hideLabel());
 
+                AjaxTextFieldPanel stringRegEx = new AjaxTextFieldPanel(
+                        "stringRegEx",
+                        "stringRegEx",
+                        new IModel<String>() {
+
+                    private static final long serialVersionUID = 
1015030402166681242L;
+
+                    @Override
+                    public String getObject() {
+                        return 
Optional.ofNullable(fpd.getStringRegEx()).map(Pattern::pattern).orElse(null);
+                    }
+
+                    @Override
+                    public void setObject(final String object) {
+                        
fpd.setStringRegEx(Optional.ofNullable(object).map(Pattern::compile).orElse(null));
+                    }
+                }, true);
+                stringRegEx.getField().add(new IValidator<String>() {
+
+                    private static final long serialVersionUID = 
3978328825079032964L;
+
+                    @Override
+                    public void validate(final IValidatable<String> 
validatable) {
+                        try {
+                            Pattern.compile(validatable.getValue());
+                        } catch (PatternSyntaxException e) {
+                            validatable.error(new ValidationError(fpd.getKey() 
+ ": invalid RegEx"));
+                        }
+                    }
+                });
+                stringRegEx.setVisible(fpd.getType() == 
FormPropertyType.String);
+                
item.add(stringRegEx.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+
                 AjaxTextFieldPanel datePattern = new AjaxTextFieldPanel(
                         "datePattern",
                         "datePattern",
                         new PropertyModel<>(fpd, "datePattern"),
                         true);
-                datePattern.setEnabled(fpd.getType() == FormPropertyType.Date);
-                item.add(datePattern.hideLabel().setOutputMarkupId(true));
+                datePattern.setVisible(fpd.getType() == FormPropertyType.Date);
+                
item.add(datePattern.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
 
                 AjaxGridFieldPanel<String, String> enumValues = new 
AjaxGridFieldPanel<>(
                         "enumValues",
                         "enumValues",
                         new PropertyModel<>(fpd, "enumValues"));
-                enumValues.setEnabled(fpd.getType() == FormPropertyType.Enum);
-                item.add(enumValues.hideLabel().setOutputMarkupId(true));
+                enumValues.setVisible(fpd.getType() == FormPropertyType.Enum);
+                
item.add(enumValues.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+
+                WebMarkupContainer dropdownConf = new 
WebMarkupContainer("dropdownConf");
+                dropdownConf.setVisible(fpd.getType() == 
FormPropertyType.Dropdown);
+                
item.add(dropdownConf.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+                AjaxCheckBoxPanel dropdownSingleSelection = new 
AjaxCheckBoxPanel(
+                        "dropdownSingleSelection",
+                        "dropdownSingleSelection",
+                        new PropertyModel<>(fpd, "dropdownSingleSelection"),
+                        true);
+                
dropdownConf.add(dropdownSingleSelection.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+                AjaxCheckBoxPanel dropdownFreeForm = new AjaxCheckBoxPanel(
+                        "dropdownFreeForm",
+                        "dropdownFreeForm",
+                        new PropertyModel<>(fpd, "dropdownFreeForm"),
+                        true);
+                
dropdownConf.add(dropdownFreeForm.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
 
                 type.getField().add(new 
IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
 
@@ -149,30 +203,59 @@ public class FormPropertyDefsPanel extends 
AbstractModalPanel<MacroTaskTO> {
                     @Override
                     protected void onUpdate(final AjaxRequestTarget target) {
                         switch (type.getModelObject()) {
+                            case String -> {
+                                stringRegEx.setVisible(true);
+                                datePattern.setVisible(false);
+                                enumValues.setVisible(false);
+                                fpd.getEnumValues().clear();
+                                dropdownConf.setVisible(false);
+                            }
+
                             case Date -> {
-                                datePattern.setEnabled(true);
-                                enumValues.setEnabled(false);
+                                stringRegEx.setVisible(false);
+                                fpd.setStringRegEx(null);
+                                datePattern.setVisible(true);
+                                enumValues.setVisible(false);
                                 fpd.getEnumValues().clear();
+                                dropdownConf.setVisible(false);
                             }
 
                             case Enum -> {
-                                datePattern.setEnabled(false);
-                                enumValues.setEnabled(true);
+                                stringRegEx.setVisible(false);
+                                fpd.setStringRegEx(null);
+                                datePattern.setVisible(false);
+                                enumValues.setVisible(true);
+                                dropdownConf.setVisible(false);
+                            }
+
+                            case Dropdown -> {
+                                stringRegEx.setVisible(false);
+                                fpd.setStringRegEx(null);
+                                datePattern.setVisible(false);
+                                enumValues.setVisible(false);
+                                fpd.getEnumValues().clear();
+                                dropdownConf.setVisible(true);
                             }
 
                             default -> {
-                                datePattern.setEnabled(false);
-                                enumValues.setEnabled(false);
+                                stringRegEx.setVisible(false);
+                                fpd.setStringRegEx(null);
+                                datePattern.setVisible(false);
+                                enumValues.setVisible(false);
                                 fpd.getEnumValues().clear();
+                                dropdownConf.setVisible(false);
                             }
                         }
 
+                        target.add(stringRegEx);
                         target.add(datePattern);
                         target.add(enumValues);
+                        target.add(dropdownConf);
                     }
                 });
 
-                ActionsPanel<Serializable> actions = new 
ActionsPanel<>("toRemove", null);
+                ActionsPanel<Serializable> actions = new 
ActionsPanel<>("actions", null);
+                item.add(actions);
                 actions.add(new ActionLink<>() {
 
                     private static final long serialVersionUID = 
-3722207913631435501L;
@@ -180,15 +263,48 @@ public class FormPropertyDefsPanel extends 
AbstractModalPanel<MacroTaskTO> {
                     @Override
                     public void onClick(final AjaxRequestTarget target, final 
Serializable ignore) {
                         model.getObject().remove(item.getIndex());
+
                         item.getParent().removeAll();
                         target.add(propertyDefContainer);
                     }
                 }, ActionLink.ActionType.DELETE, StringUtils.EMPTY, 
true).hideLabel();
-                item.add(actions);
+                if (model.getObject().size() > 1) {
+                    if (item.getIndex() > 0) {
+                        actions.add(new ActionLink<>() {
+
+                            private static final long serialVersionUID = 
2041211756396714619L;
+
+                            @Override
+                            public void onClick(final AjaxRequestTarget 
target, final Serializable ignore) {
+                                FormPropertyDefTO pre = 
model.getObject().get(item.getIndex() - 1);
+                                model.getObject().set(item.getIndex(), pre);
+                                model.getObject().set(item.getIndex() - 1, 
fpd);
+
+                                item.getParent().removeAll();
+                                target.add(propertyDefContainer);
+                            }
+                        }, ActionLink.ActionType.UP, 
StringUtils.EMPTY).hideLabel();
+                    }
+                    if (item.getIndex() < model.getObject().size() - 1) {
+                        actions.add(new ActionLink<>() {
+
+                            private static final long serialVersionUID = 
2041211756396714619L;
+
+                            @Override
+                            public void onClick(final AjaxRequestTarget 
target, final Serializable ignore) {
+                                FormPropertyDefTO post = 
model.getObject().get(item.getIndex() + 1);
+                                model.getObject().set(item.getIndex(), post);
+                                model.getObject().set(item.getIndex() + 1, 
fpd);
+
+                                item.getParent().removeAll();
+                                target.add(propertyDefContainer);
+                            }
+                        }, ActionLink.ActionType.DOWN, 
StringUtils.EMPTY).hideLabel();
+                    }
+                }
             }
         };
-        propertyDefs.setReuseItems(true);
-        propertyDefContainer.add(propertyDefs);
+        propertyDefContainer.add(propertyDefs.setReuseItems(true));
 
         IndicatingAjaxButton addPropertyDef = new 
IndicatingAjaxButton("addPropertyDef") {
 
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
index 345fda7a3b..31cd627472 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
@@ -138,6 +138,7 @@ public class MacroTaskDirectoryPanel extends 
SchedTaskDirectoryPanel<MacroTaskTO
 
             @Override
             public void onClick(final AjaxRequestTarget target, final 
MacroTaskTO ignore) {
+                model.setObject(restClient.readTask(TaskType.MACRO, 
model.getObject().getKey()));
                 MacroTaskExecWizardBuilder wb = new 
MacroTaskExecWizardBuilder(model.getObject(), restClient, pageRef);
                 wb.setEventSink(new ExecModalEventSink());
 
@@ -164,6 +165,7 @@ public class MacroTaskDirectoryPanel extends 
SchedTaskDirectoryPanel<MacroTaskTO
 
             @Override
             public void onClick(final AjaxRequestTarget target, final 
MacroTaskTO ignore) {
+                model.setObject(restClient.readTask(TaskType.MACRO, 
model.getObject().getKey()));
                 target.add(modal.setContent(new CommandComposeDirectoryPanel(
                         model.getObject().getKey(), commandRestClient, modal, 
pageRef)));
 
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/ConsoleAuxClasses.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/ConsoleAuxClasses.java
index 0fb1f700a0..bdb8a6168f 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/ConsoleAuxClasses.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/ConsoleAuxClasses.java
@@ -38,7 +38,7 @@ public class ConsoleAuxClasses extends AbstractAuxClasses {
     }
 
     @Override
-    protected final List<AnyTypeClassTO> listAnyTypecClasses() {
+    protected List<AnyTypeClassTO> listAnyTypecClasses() {
         return anyTypeClassRestClient.list();
     }
 }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
index 735fb6d73d..096e813744 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
@@ -45,9 +45,17 @@ import org.slf4j.LoggerFactory;
 
 public class Details<T extends AnyTO> extends WizardStep {
 
+    private static final long serialVersionUID = -8995647450549098844L;
+
     protected static final Logger LOG = LoggerFactory.getLogger(Details.class);
 
-    private static final long serialVersionUID = -8995647450549098844L;
+    protected static List<RealmTO> getRealmsFromLinks(final List<AbstractLink> 
realmLinks) {
+        return realmLinks.stream().
+                map(Component::getDefaultModelObject).
+                filter(RealmTO.class::isInstance).
+                map(RealmTO.class::cast).
+                toList();
+    }
 
     @SpringBean
     protected RealmRestClient realmRestClient;
@@ -113,12 +121,4 @@ public class Details<T extends AnyTO> extends WizardStep {
     protected AnnotatedBeanPanel getGeneralStatusInformation(final String id, 
final T anyTO) {
         return new AnnotatedBeanPanel(id, anyTO);
     }
-
-    private static List<RealmTO> getRealmsFromLinks(final List<AbstractLink> 
realmLinks) {
-        return realmLinks.stream().
-                map(Component::getDefaultModelObject).
-                filter(RealmTO.class::isInstance).
-                map(RealmTO.class::cast).
-                collect(Collectors.toList());
-    }
 }
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyMacroActions.groovy
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyMacroActions.groovy
index c920b030a7..354fa91ea2 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyMacroActions.groovy
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyMacroActions.groovy
@@ -18,9 +18,10 @@
  */
 import groovy.transform.CompileStatic
 import java.util.Map
-import javax.xml.bind.ValidationException
+import java.util.Optional
+import javax.validation.ValidationException
 import org.apache.syncope.common.lib.command.CommandArgs
-import org.apache.syncope.common.lib.form.MacroTaskForm
+import org.apache.syncope.common.lib.form.SyncopeForm
 import org.apache.syncope.core.provisioning.api.macro.Command
 import org.apache.syncope.core.provisioning.api.macro.MacroActions
 
@@ -28,14 +29,19 @@ import 
org.apache.syncope.core.provisioning.api.macro.MacroActions
 class MyMacroActions implements MacroActions {
 
   @Override
-  void validate(MacroTaskForm macroTaskForm) throws ValidationException {
+  Optional<String> getDefaultValue(String formProperty) {
+    return Optional.empty();
   }
-  
+
   @Override
   Map<String, String> getDropdownValues(String formProperty) {
-    return Map.of();
+    return Map.of()
   }
   
+  @Override
+  void validate(SyncopeForm form, Map<String, Object> vars) throws 
ValidationException {
+  }
+
   @Override
   void beforeAll() {
   }
@@ -50,6 +56,6 @@ class MyMacroActions implements MacroActions {
 
   @Override
   StringBuilder afterAll(StringBuilder output) {
-    return output;
+    return output
   }
 }
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.html
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.html
index f40c2b55d7..933b16e786 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.html
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.html
@@ -27,12 +27,11 @@ under the License.
           <tr>
             <th><wicket:message key="key"/></th>
             <th><wicket:message key="name"/></th>
-            <th><wicket:message key="type"/></th>
             <th><wicket:message key="readable"/></th>
             <th><wicket:message key="writable"/></th>
             <th><wicket:message key="required"/></th>
-            <th><wicket:message key="datePattern"/></th>
-            <th><wicket:message key="enumValues"/></th>
+            <th><wicket:message key="type"/></th>
+            <th><wicket:message key="conf"/></th>
             <th></th>
           </tr>
 
@@ -43,9 +42,6 @@ under the License.
             <td>
               <span wicket:id="name"/>
             </td>
-            <td>
-              <span wicket:id="type"/>
-            </td>
             <td>
               <span wicket:id="readable"/>
             </td>
@@ -56,14 +52,20 @@ under the License.
               <span wicket:id="required"/>
             </td>
             <td>
-              <span wicket:id="datePattern"/>
+              <span wicket:id="type"/>
             </td>
             <td>
+              <span wicket:id="stringRegEx"/>
+              <span wicket:id="datePattern"/>
               <span wicket:id="enumValues"/>
+              <div wicket:id="dropdownConf">
+                <span wicket:id="dropdownSingleSelection"/>
+                <span wicket:id="dropdownFreeForm"/>
+              </div>
             </td>
             <td>
               <div id="inline-actions">
-                <span wicket:id="toRemove"/>
+                <span wicket:id="actions"/>
               </div>
             </td>
           </tr>
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.properties
index 8ba3a3a829..5831fc80ca 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel.properties
@@ -18,5 +18,9 @@ type=Type
 readable=Readable
 writable=Writable
 required=Required
-datePattern=Date pattern
-enumValues=Enum values
+conf=Configuration
+stringRegEx=Regular Expression
+enumValues=Values
+datePattern=Pattern
+dropdownSingleSelection=Single Selection
+dropdownFreeForm=Free Form
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_fr_CA.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_fr_CA.properties
index 8ba3a3a829..5831fc80ca 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_fr_CA.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_fr_CA.properties
@@ -18,5 +18,9 @@ type=Type
 readable=Readable
 writable=Writable
 required=Required
-datePattern=Date pattern
-enumValues=Enum values
+conf=Configuration
+stringRegEx=Regular Expression
+enumValues=Values
+datePattern=Pattern
+dropdownSingleSelection=Single Selection
+dropdownFreeForm=Free Form
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_it.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_it.properties
index 62a3a2d422..5cd41d2b8b 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_it.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_it.properties
@@ -18,5 +18,9 @@ type=Tipo
 readable=Lettura
 writable=Scrittura
 required=Obbligatorio
-datePattern=Modello data
-enumValues=Valori enum
+conf=Configurazione
+stringRegEx=Espressione Regolare
+enumValues=Valori
+datePattern=Modello
+dropdownSingleSelection=Selezione Singola
+dropdownFreeForm=Modalit\u00e0 libera
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ja.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ja.properties
index 8ba3a3a829..5831fc80ca 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ja.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ja.properties
@@ -18,5 +18,9 @@ type=Type
 readable=Readable
 writable=Writable
 required=Required
-datePattern=Date pattern
-enumValues=Enum values
+conf=Configuration
+stringRegEx=Regular Expression
+enumValues=Values
+datePattern=Pattern
+dropdownSingleSelection=Single Selection
+dropdownFreeForm=Free Form
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_pt_BR.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_pt_BR.properties
index 8ba3a3a829..5831fc80ca 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_pt_BR.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_pt_BR.properties
@@ -18,5 +18,9 @@ type=Type
 readable=Readable
 writable=Writable
 required=Required
-datePattern=Date pattern
-enumValues=Enum values
+conf=Configuration
+stringRegEx=Regular Expression
+enumValues=Values
+datePattern=Pattern
+dropdownSingleSelection=Single Selection
+dropdownFreeForm=Free Form
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ru.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ru.properties
index 8ba3a3a829..5831fc80ca 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ru.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/FormPropertyDefsPanel_ru.properties
@@ -18,5 +18,9 @@ type=Type
 readable=Readable
 writable=Writable
 required=Required
-datePattern=Date pattern
-enumValues=Enum values
+conf=Configuration
+stringRegEx=Regular Expression
+enumValues=Values
+datePattern=Pattern
+dropdownSingleSelection=Single Selection
+dropdownFreeForm=Free Form
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
index 0e53e9e9f2..b8d5d685ee 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
@@ -276,3 +276,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 explore_resource.title=explore resource
 explore_resource.alt=explore resource icon
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=move up
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=move down
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
index 48d13be1b2..b42f43a855 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_fr_CA.properties
@@ -221,3 +221,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 explore_resource.title=explorer la ressource
 explore_resource.alt=ic\u00f4ne explorer la ressource
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=move up
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=move down
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
index dbbab266f3..e5d20e0446 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
@@ -276,3 +276,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 explore_resource.title=esplora risorsa
 explore_resource.alt=explore resource icon
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=sposta su
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=sposta gi\u00f9
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
index 12f5a73617..1a97e92fb5 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
@@ -277,3 +277,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 explore_resource.title=\u30ea\u30bd\u30fc\u30b9\u3092\u63a2\u7d22\u3059\u308b
 
explore_resource.alt=\u30ea\u30bd\u30fc\u30b9\u30a2\u30a4\u30b3\u30f3\u3092\u63a2\u3059
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=move up
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=move down
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
index 368df37cc3..dbb9c181a4 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
@@ -281,3 +281,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 explore_resource.title=explorar recurso
 explore_resource.alt=\u00edcone de explorar recurso
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=move up
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=move down
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
index 822849583a..7277a38550 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
@@ -277,3 +277,9 @@ merge_accounts.alt=merge accounts icon
 explore_resource.class=fa fa-eye
 
explore_resource.title=\u0438\u0437\u0443\u0447\u0438\u0442\u044c\u0020\u0440\u0435\u0441\u0443\u0440\u0441
 
explore_resource.alt=\u0438\u0437\u0443\u0447\u0438\u0442\u044c\u0020\u0437\u043d\u0430\u0447\u043e\u043a\u0020\u0440\u0435\u0441\u0443\u0440\u0441\u0430
+up.alt=up icon
+up.class=fas fa-arrow-up
+up.title=move up
+down.alt=down icon
+down.class=fas fa-arrow-down
+down.title=move down
diff --git 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/form/FormProperty.java
 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/form/FormProperty.java
index 471f072870..dbf8745ef9 100644
--- 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/form/FormProperty.java
+++ 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/form/FormProperty.java
@@ -23,6 +23,7 @@ import 
com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Pattern;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
@@ -42,12 +43,18 @@ public class FormProperty implements Serializable {
 
     private boolean required;
 
+    private Pattern stringRegEx;
+
     private String datePattern;
 
     private final List<FormPropertyValue> enumValues = new ArrayList<>();
 
     private final List<FormPropertyValue> dropdownValues = new ArrayList<>();
 
+    private boolean dropdownSingleSelection = true;
+
+    private boolean dropdownFreeForm;
+
     private String value;
 
     public String getId() {
@@ -82,6 +89,14 @@ public class FormProperty implements Serializable {
         this.required = required;
     }
 
+    public Pattern getStringRegEx() {
+        return stringRegEx;
+    }
+
+    public void setStringRegEx(final Pattern stringRegEx) {
+        this.stringRegEx = stringRegEx;
+    }
+
     public FormPropertyType getType() {
         return type;
     }
@@ -118,6 +133,22 @@ public class FormProperty implements Serializable {
         return dropdownValues;
     }
 
+    public boolean isDropdownSingleSelection() {
+        return dropdownSingleSelection;
+    }
+
+    public void setDropdownSingleSelection(final boolean 
dropdownSingleSelection) {
+        this.dropdownSingleSelection = dropdownSingleSelection;
+    }
+
+    public boolean isDropdownFreeForm() {
+        return dropdownFreeForm;
+    }
+
+    public void setDropdownFreeForm(final boolean dropdownFreeForm) {
+        this.dropdownFreeForm = dropdownFreeForm;
+    }
+
     public String getValue() {
         return value;
     }
@@ -135,9 +166,12 @@ public class FormProperty implements Serializable {
                 append(readable).
                 append(writable).
                 append(required).
+                append(stringRegEx).
                 append(datePattern).
                 append(enumValues).
                 append(dropdownValues).
+                append(dropdownSingleSelection).
+                append(dropdownFreeForm).
                 append(value).
                 build();
     }
@@ -161,9 +195,12 @@ public class FormProperty implements Serializable {
                 append(readable, other.readable).
                 append(writable, other.writable).
                 append(required, other.required).
+                append(stringRegEx, other.stringRegEx).
                 append(datePattern, other.datePattern).
                 append(enumValues, other.enumValues).
                 append(dropdownValues, other.dropdownValues).
+                append(dropdownSingleSelection, other.dropdownSingleSelection).
+                append(dropdownFreeForm, other.dropdownFreeForm).
                 append(value, other.value).
                 build();
     }
diff --git 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/FormPropertyDefTO.java
 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/FormPropertyDefTO.java
index 5357b1df39..4f1d24aba5 100644
--- 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/FormPropertyDefTO.java
+++ 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/FormPropertyDefTO.java
@@ -20,6 +20,7 @@ package org.apache.syncope.common.lib.to;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.regex.Pattern;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.syncope.common.lib.form.FormPropertyType;
@@ -40,10 +41,16 @@ public class FormPropertyDefTO implements NamedEntityTO {
 
     private boolean required;
 
+    private Pattern stringRegEx;
+
     private String datePattern;
 
     private final Map<String, String> enumValues = new LinkedHashMap<>();
 
+    private boolean dropdownSingleSelection = true;
+
+    private boolean dropdownFreeForm;
+
     @Override
     public String getKey() {
         return key;
@@ -96,6 +103,14 @@ public class FormPropertyDefTO implements NamedEntityTO {
         this.required = required;
     }
 
+    public Pattern getStringRegEx() {
+        return stringRegEx;
+    }
+
+    public void setStringRegEx(final Pattern stringRegEx) {
+        this.stringRegEx = stringRegEx;
+    }
+
     public String getDatePattern() {
         return datePattern;
     }
@@ -108,6 +123,22 @@ public class FormPropertyDefTO implements NamedEntityTO {
         return enumValues;
     }
 
+    public boolean isDropdownSingleSelection() {
+        return dropdownSingleSelection;
+    }
+
+    public void setDropdownSingleSelection(final boolean 
dropdownSingleSelection) {
+        this.dropdownSingleSelection = dropdownSingleSelection;
+    }
+
+    public boolean isDropdownFreeForm() {
+        return dropdownFreeForm;
+    }
+
+    public void setDropdownFreeForm(final boolean dropdownFreeForm) {
+        this.dropdownFreeForm = dropdownFreeForm;
+    }
+
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
@@ -117,8 +148,11 @@ public class FormPropertyDefTO implements NamedEntityTO {
                 append(readable).
                 append(writable).
                 append(required).
+                append(stringRegEx).
                 append(datePattern).
                 append(enumValues).
+                append(dropdownSingleSelection).
+                append(dropdownFreeForm).
                 build();
     }
 
@@ -141,8 +175,11 @@ public class FormPropertyDefTO implements NamedEntityTO {
                 append(readable, other.readable).
                 append(writable, other.writable).
                 append(required, other.required).
+                append(stringRegEx, other.stringRegEx).
                 append(datePattern, other.datePattern).
                 append(enumValues, other.enumValues).
+                append(dropdownSingleSelection, other.dropdownSingleSelection).
+                append(dropdownFreeForm, other.dropdownFreeForm).
                 build();
     }
 }
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
index 3030630e14..99c57b1a19 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
@@ -420,7 +420,8 @@ public class TaskLogic extends 
AbstractExecutableLogic<TaskTO> {
 
         if (TaskType.SCHEDULED == taskUtils.getType()
                 || TaskType.PULL == taskUtils.getType()
-                || TaskType.PUSH == taskUtils.getType()) {
+                || TaskType.PUSH == taskUtils.getType()
+                || TaskType.MACRO == taskUtils.getType()) {
 
             jobManager.unregister(task);
         }
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/FormPropertyDef.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/FormPropertyDef.java
index dbfcadd620..518ab750d7 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/FormPropertyDef.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/FormPropertyDef.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.core.persistence.api.entity.task;
 
 import java.util.Map;
+import java.util.regex.Pattern;
 import org.apache.syncope.common.lib.form.FormPropertyType;
 import org.apache.syncope.core.persistence.api.entity.ProvidedKeyEntity;
 
@@ -48,6 +49,10 @@ public interface FormPropertyDef extends ProvidedKeyEntity {
 
     void setRequired(boolean required);
 
+    Pattern getStringRegEx();
+
+    void setStringRegExp(Pattern stringRegEx);
+
     String getDatePattern();
 
     void setDatePattern(String datePattern);
@@ -55,4 +60,12 @@ public interface FormPropertyDef extends ProvidedKeyEntity {
     Map<String, String> getEnumValues();
 
     void setEnumValues(Map<String, String> enumValues);
+
+    boolean isDropdownSingleSelection();
+
+    void setDropdownSingleSelection(boolean dropdownSingleSelection);
+
+    boolean isDropdownFreeForm();
+
+    void setDropdownFreeForm(boolean dropdownFreeForm);
 }
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAFormPropertyDef.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAFormPropertyDef.java
index 0d86db3fa7..f877ddacee 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAFormPropertyDef.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAFormPropertyDef.java
@@ -28,6 +28,7 @@ import jakarta.persistence.Table;
 import jakarta.validation.constraints.NotNull;
 import java.util.Map;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import org.apache.syncope.common.lib.form.FormPropertyType;
 import org.apache.syncope.core.persistence.api.entity.task.FormPropertyDef;
 import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
@@ -44,6 +45,11 @@ public class JPAFormPropertyDef extends 
AbstractProvidedKeyEntity implements For
 
     public static final String TABLE = "FormPropertyDef";
 
+    protected static final TypeReference<Map<String, String>> TYPEREF = new 
TypeReference<Map<String, String>>() {
+    };
+
+    private int idx;
+
     @ManyToOne(optional = false)
     private JPAMacroTask macroTask;
 
@@ -63,11 +69,23 @@ public class JPAFormPropertyDef extends 
AbstractProvidedKeyEntity implements For
     @NotNull
     private Boolean required = Boolean.FALSE;
 
+    private String stringRegEx;
+
     private String datePattern;
 
     @Lob
     private String enumValues;
 
+    @NotNull
+    private Boolean dropdownSingleSelection = Boolean.TRUE;
+
+    @NotNull
+    private Boolean dropdownFreeForm = Boolean.FALSE;
+
+    public void setIdx(final int idx) {
+        this.idx = idx;
+    }
+
     @Override
     public JPAMacroTask getMacroTask() {
         return macroTask;
@@ -129,6 +147,16 @@ public class JPAFormPropertyDef extends 
AbstractProvidedKeyEntity implements For
         this.required = required;
     }
 
+    @Override
+    public Pattern getStringRegEx() {
+        return 
Optional.ofNullable(stringRegEx).map(Pattern::compile).orElse(null);
+    }
+
+    @Override
+    public void setStringRegExp(final Pattern stringRegEx) {
+        this.stringRegEx = 
Optional.ofNullable(stringRegEx).map(Pattern::pattern).orElse(null);
+    }
+
     @Override
     public String getDatePattern() {
         return datePattern;
@@ -141,13 +169,31 @@ public class JPAFormPropertyDef extends 
AbstractProvidedKeyEntity implements For
 
     @Override
     public Map<String, String> getEnumValues() {
-        return Optional.ofNullable(enumValues).
-                map(v -> POJOHelper.deserialize(v, new 
TypeReference<Map<String, String>>() {
-        })).orElse(Map.of());
+        return Optional.ofNullable(enumValues).map(v -> 
POJOHelper.deserialize(v, TYPEREF)).orElse(Map.of());
     }
 
     @Override
     public void setEnumValues(final Map<String, String> enumValues) {
         this.enumValues = 
Optional.ofNullable(enumValues).map(POJOHelper::serialize).orElse(null);
     }
+
+    @Override
+    public boolean isDropdownSingleSelection() {
+        return dropdownSingleSelection == null ? false : 
dropdownSingleSelection;
+    }
+
+    @Override
+    public void setDropdownSingleSelection(final boolean 
dropdownSingleSelection) {
+        this.dropdownSingleSelection = dropdownSingleSelection;
+    }
+
+    @Override
+    public boolean isDropdownFreeForm() {
+        return dropdownFreeForm == null ? false : dropdownFreeForm;
+    }
+
+    @Override
+    public void setDropdownFreeForm(final boolean dropdownFreeForm) {
+        this.dropdownFreeForm = dropdownFreeForm;
+    }
 }
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
index 8660cd8e3a..64725466b5 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
 import jakarta.persistence.FetchType;
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.OneToMany;
+import jakarta.persistence.OrderBy;
 import jakarta.persistence.Table;
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotNull;
@@ -57,9 +58,11 @@ public class JPAMacroTask extends JPASchedTask implements 
MacroTask {
     private Boolean saveExecs = true;
 
     @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, 
orphanRemoval = true, mappedBy = "macroTask")
+    @OrderBy("idx")
     private List<JPAMacroTaskCommand> macroTaskCommands = new ArrayList<>();
 
     @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, 
orphanRemoval = true, mappedBy = "macroTask")
+    @OrderBy("idx")
     @Valid
     private List<JPAFormPropertyDef> formPropertyDefs = new ArrayList<>();
 
@@ -114,7 +117,8 @@ public class JPAMacroTask extends JPASchedTask implements 
MacroTask {
     @Override
     public void add(final MacroTaskCommand macroTaskCommand) {
         checkType(macroTaskCommand, JPAMacroTaskCommand.class);
-        this.macroTaskCommands.add((JPAMacroTaskCommand) macroTaskCommand);
+        ((JPAMacroTaskCommand) 
macroTaskCommand).setIdx(macroTaskCommands.size());
+        macroTaskCommands.add((JPAMacroTaskCommand) macroTaskCommand);
     }
 
     @Override
@@ -125,7 +129,8 @@ public class JPAMacroTask extends JPASchedTask implements 
MacroTask {
     @Override
     public void add(final FormPropertyDef formPropertyDef) {
         checkType(formPropertyDef, JPAFormPropertyDef.class);
-        this.formPropertyDefs.add((JPAFormPropertyDef) formPropertyDef);
+        ((JPAFormPropertyDef) formPropertyDef).setIdx(formPropertyDefs.size());
+        formPropertyDefs.add((JPAFormPropertyDef) formPropertyDef);
     }
 
     @Override
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskCommand.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskCommand.java
index df3bea0465..86ca5037c1 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskCommand.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskCommand.java
@@ -41,6 +41,8 @@ public class JPAMacroTaskCommand extends 
AbstractGeneratedKeyEntity implements M
 
     public static final String TABLE = "MacroTaskCommand";
 
+    private int idx;
+
     @ManyToOne(optional = false)
     private JPAMacroTask macroTask;
 
@@ -50,6 +52,10 @@ public class JPAMacroTaskCommand extends 
AbstractGeneratedKeyEntity implements M
     @Lob
     private String args;
 
+    public void setIdx(final int idx) {
+        this.idx = idx;
+    }
+
     @Override
     public JPAMacroTask getMacroTask() {
         return macroTask;
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDef.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDef.java
index af494c82e6..ab6f9c9caa 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDef.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDef.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import jakarta.validation.constraints.NotNull;
 import java.util.Map;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import org.apache.syncope.common.lib.form.FormPropertyType;
 import org.apache.syncope.core.persistence.api.entity.task.FormPropertyDef;
 import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
@@ -39,6 +40,9 @@ public class Neo4jFormPropertyDef extends 
AbstractProvidedKeyNode implements For
 
     public static final String NODE = "FormPropertyDef";
 
+    protected static final TypeReference<Map<String, String>> TYPEREF = new 
TypeReference<Map<String, String>>() {
+    };
+
     @NotNull
     @Relationship(type = Neo4jMacroTask.MACRO_TASK_FORM_PROPERTY_DEF_REL, 
direction = Relationship.Direction.OUTGOING)
     private Neo4jMacroTask macroTask;
@@ -58,10 +62,18 @@ public class Neo4jFormPropertyDef extends 
AbstractProvidedKeyNode implements For
     @NotNull
     private Boolean required = Boolean.FALSE;
 
+    private String stringRegEx;
+
     private String datePattern;
 
     private String enumValues;
 
+    @NotNull
+    private Boolean dropdownSingleSelection = Boolean.TRUE;
+
+    @NotNull
+    private Boolean dropdownFreeForm = Boolean.FALSE;
+
     @Override
     public Neo4jMacroTask getMacroTask() {
         return macroTask;
@@ -123,6 +135,16 @@ public class Neo4jFormPropertyDef extends 
AbstractProvidedKeyNode implements For
         this.required = required;
     }
 
+    @Override
+    public Pattern getStringRegEx() {
+        return 
Optional.ofNullable(stringRegEx).map(Pattern::compile).orElse(null);
+    }
+
+    @Override
+    public void setStringRegExp(final Pattern stringRegEx) {
+        this.stringRegEx = 
Optional.ofNullable(stringRegEx).map(Pattern::pattern).orElse(null);
+    }
+
     @Override
     public String getDatePattern() {
         return datePattern;
@@ -135,13 +157,31 @@ public class Neo4jFormPropertyDef extends 
AbstractProvidedKeyNode implements For
 
     @Override
     public Map<String, String> getEnumValues() {
-        return Optional.ofNullable(enumValues).
-                map(v -> POJOHelper.deserialize(v, new 
TypeReference<Map<String, String>>() {
-        })).orElse(Map.of());
+        return Optional.ofNullable(enumValues).map(v -> 
POJOHelper.deserialize(v, TYPEREF)).orElse(Map.of());
     }
 
     @Override
     public void setEnumValues(final Map<String, String> enumValues) {
         this.enumValues = 
Optional.ofNullable(enumValues).map(POJOHelper::serialize).orElse(null);
     }
+
+    @Override
+    public boolean isDropdownSingleSelection() {
+        return dropdownSingleSelection == null ? false : 
dropdownSingleSelection;
+    }
+
+    @Override
+    public void setDropdownSingleSelection(final boolean 
dropdownSingleSelection) {
+        this.dropdownSingleSelection = dropdownSingleSelection;
+    }
+
+    @Override
+    public boolean isDropdownFreeForm() {
+        return dropdownFreeForm == null ? false : dropdownFreeForm;
+    }
+
+    @Override
+    public void setDropdownFreeForm(final boolean dropdownFreeForm) {
+        this.dropdownFreeForm = dropdownFreeForm;
+    }
 }
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java
new file mode 100644
index 0000000000..be1cfc5fb7
--- /dev/null
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.neo4j.entity.task;
+
+import jakarta.validation.Valid;
+import java.util.function.BiFunction;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import 
org.apache.syncope.core.persistence.neo4j.entity.Neo4jSortedRelationsihip;
+import org.springframework.data.neo4j.core.schema.RelationshipId;
+import org.springframework.data.neo4j.core.schema.RelationshipProperties;
+import org.springframework.data.neo4j.core.schema.TargetNode;
+
+@RelationshipProperties
+public class Neo4jFormPropertyDefRelationship
+        extends Neo4jSortedRelationsihip<Neo4jFormPropertyDef>
+        implements Comparable<Neo4jFormPropertyDefRelationship> {
+
+    public static BiFunction<Integer, Neo4jFormPropertyDef, 
Neo4jFormPropertyDefRelationship> builder() {
+        return (Integer i, Neo4jFormPropertyDef e) -> new 
Neo4jFormPropertyDefRelationship(i, e);
+    }
+
+    @RelationshipId
+    private Long id;
+
+    private int index;
+
+    @TargetNode
+    @Valid
+    private Neo4jFormPropertyDef formPropertyDef;
+
+    public Neo4jFormPropertyDefRelationship(final int index, final 
Neo4jFormPropertyDef formPropertyDef) {
+        this.index = index;
+        this.formPropertyDef = formPropertyDef;
+    }
+
+    @Override
+    public int getIndex() {
+        return index;
+    }
+
+    @Override
+    public Neo4jFormPropertyDef getEntity() {
+        return formPropertyDef;
+    }
+
+    @Override
+    public int compareTo(final Neo4jFormPropertyDefRelationship object) {
+        return Integer.compare(index, object.getIndex());
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Neo4jFormPropertyDefRelationship other = 
(Neo4jFormPropertyDefRelationship) obj;
+        return new EqualsBuilder().
+                append(index, other.index).
+                append(formPropertyDef, other.formPropertyDef).
+                build();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(index).
+                append(formPropertyDef).
+                build();
+    }
+
+    @Override
+    public String toString() {
+        return "Neo4jMacroTaskCommandRelationship{"
+                + "id=" + id
+                + ", index=" + index
+                + ", formPropertyDef=" + formPropertyDef
+                + '}';
+    }
+}
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTask.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTask.java
index ff830350df..8427d08c85 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTask.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTask.java
@@ -74,7 +74,11 @@ public class Neo4jMacroTask extends Neo4jSchedTask 
implements MacroTask {
 
     @Relationship(type = MACRO_TASK_FORM_PROPERTY_DEF_REL, direction = 
Relationship.Direction.INCOMING)
     @Valid
-    private List<Neo4jFormPropertyDef> formPropertyDefs = new ArrayList<>();
+    private SortedSet<Neo4jFormPropertyDefRelationship> formPropertyDefs = new 
TreeSet<>();
+
+    @Transient
+    private List<Neo4jFormPropertyDef> sortedFormPropertyDefs = new 
SortedSetList<>(
+            formPropertyDefs, Neo4jFormPropertyDefRelationship.builder());
 
     @Relationship(type = MACRO_TASK_MACRO_ACTIONS_REL, direction = 
Relationship.Direction.OUTGOING)
     private Neo4jImplementation macroActions;
@@ -142,12 +146,12 @@ public class Neo4jMacroTask extends Neo4jSchedTask 
implements MacroTask {
     @Override
     public void add(final FormPropertyDef formPropertyDef) {
         checkType(formPropertyDef, Neo4jFormPropertyDef.class);
-        this.formPropertyDefs.add((Neo4jFormPropertyDef) formPropertyDef);
+        sortedFormPropertyDefs.add((Neo4jFormPropertyDef) formPropertyDef);
     }
 
     @Override
     public List<? extends FormPropertyDef> getFormPropertyDefs() {
-        return formPropertyDefs;
+        return sortedFormPropertyDefs;
     }
 
     @Override
@@ -165,5 +169,6 @@ public class Neo4jMacroTask extends Neo4jSchedTask 
implements MacroTask {
     @PostLoad
     public void postLoad() {
         sortedCommands = new SortedSetList<>(commands, 
Neo4jMacroTaskCommandRelationship.builder());
+        sortedFormPropertyDefs = new SortedSetList<>(formPropertyDefs, 
Neo4jFormPropertyDefRelationship.builder());
     }
 }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/macro/MacroActions.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/macro/MacroActions.java
index a631586731..b0de301083 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/macro/MacroActions.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/macro/MacroActions.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.provisioning.api.macro;
 
 import jakarta.validation.ValidationException;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.syncope.common.lib.command.CommandArgs;
 import org.apache.syncope.common.lib.form.SyncopeForm;
 
@@ -28,14 +29,18 @@ import org.apache.syncope.common.lib.form.SyncopeForm;
  */
 public interface MacroActions {
 
-    default void validate(SyncopeForm macroTaskForm) throws 
ValidationException {
-        // does nothing by default
+    default Optional<String> getDefaultValue(String formProperty) {
+        return Optional.empty();
     }
 
     default Map<String, String> getDropdownValues(String formProperty) {
         return Map.of();
     }
 
+    default void validate(SyncopeForm form, Map<String, Object> vars) throws 
ValidationException {
+        // does nothing by default
+    }
+
     default void beforeAll() {
         // does nothing by default
     }
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ImplementationDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ImplementationDataBinderImpl.java
index b92d98ebc8..53eec0b341 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ImplementationDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ImplementationDataBinderImpl.java
@@ -34,6 +34,7 @@ import 
org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.provisioning.api.data.ImplementationDataBinder;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -133,6 +134,15 @@ public class ImplementationDataBinderImpl implements 
ImplementationDataBinder {
                     }
                     break;
             }
+        } else if (implementation.getEngine() == ImplementationEngine.GROOVY) {
+            try {
+                ImplementationManager.build(implementation);
+            } catch (Exception e) {
+                LOG.error("While building Groovy class {}", 
implementation.getKey(), e);
+
+                sce.getElements().add(e.getMessage());
+                throw sce;
+            }
         }
     }
 
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
index d4eb8492a6..2c19d573aa 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
@@ -275,9 +275,12 @@ public class TaskDataBinderImpl extends 
AbstractExecutableDatabinder implements
             fpd.setType(fpdTO.getType());
             fpd.setReadable(fpdTO.isReadable());
             fpd.setWritable(fpdTO.isWritable());
+            fpd.setStringRegExp(fpdTO.getStringRegEx());
             fpd.setRequired(fpdTO.isRequired());
             fpd.setDatePattern(fpdTO.getDatePattern());
             fpd.setEnumValues(fpdTO.getEnumValues());
+            fpd.setDropdownSingleSelection(fpdTO.isDropdownSingleSelection());
+            fpd.setDropdownFreeForm(fpdTO.isDropdownFreeForm());
 
             fpd.setMacroTask(macroTask);
             macroTask.add(fpd);
@@ -500,8 +503,11 @@ public class TaskDataBinderImpl extends 
AbstractExecutableDatabinder implements
                     fpdTO.setReadable(fpd.isReadable());
                     fpdTO.setWritable(fpd.isWritable());
                     fpdTO.setRequired(fpd.isRequired());
+                    fpdTO.setStringRegEx(fpd.getStringRegEx());
                     fpdTO.setDatePattern(fpd.getDatePattern());
                     fpdTO.getEnumValues().putAll(fpd.getEnumValues());
+                    
fpdTO.setDropdownSingleSelection(fpd.isDropdownSingleSelection());
+                    fpdTO.setDropdownFreeForm(fpd.isDropdownFreeForm());
 
                     macroTaskTO.getFormPropertyDefs().add(fpdTO);
                 });
@@ -608,7 +614,11 @@ public class TaskDataBinderImpl extends 
AbstractExecutableDatabinder implements
             prop.setRequired(fpd.isRequired());
             prop.setWritable(fpd.isWritable());
             prop.setType(fpd.getType());
+            actions.flatMap(a -> a.getDefaultValue(fpd.getKey())).ifPresent(v 
-> prop.setValue(v));
             switch (prop.getType()) {
+                case String ->
+                    prop.setStringRegEx(fpd.getStringRegEx());
+
                 case Date ->
                     prop.setDatePattern(fpd.getDatePattern());
 
@@ -616,9 +626,12 @@ public class TaskDataBinderImpl extends 
AbstractExecutableDatabinder implements
                     fpd.getEnumValues().
                             forEach((key, value) -> 
prop.getEnumValues().add(new FormPropertyValue(key, value)));
 
-                case Dropdown ->
+                case Dropdown -> {
                     actions.ifPresent(a -> a.getDropdownValues(fpd.getKey()).
                             forEach((key, value) -> 
prop.getDropdownValues().add(new FormPropertyValue(key, value))));
+                    
prop.setDropdownSingleSelection(fpd.isDropdownSingleSelection());
+                    prop.setDropdownFreeForm(fpd.isDropdownFreeForm());
+                }
 
                 default -> {
                 }
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
index e563026212..69e1a26856 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
@@ -22,7 +22,9 @@ import jakarta.annotation.Resource;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ValidationException;
 import jakarta.validation.Validator;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -39,6 +41,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.command.CommandArgs;
+import org.apache.syncope.common.lib.form.FormProperty;
 import org.apache.syncope.common.lib.form.SyncopeForm;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.entity.task.FormPropertyDef;
@@ -54,6 +57,7 @@ import 
org.apache.syncope.core.provisioning.api.macro.MacroActions;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.apache.syncope.core.spring.task.VirtualThreadPoolTaskExecutor;
+import org.springframework.aop.support.AopUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import 
org.springframework.security.concurrent.DelegatingSecurityContextCallable;
 import org.springframework.util.ReflectionUtils;
@@ -75,21 +79,6 @@ public class MacroJobDelegate extends 
AbstractSchedTaskJobDelegate<MacroTask> {
 
     protected final Map<String, Command<?>> perContextCommands = new 
ConcurrentHashMap<>();
 
-    protected boolean validate(final FormPropertyDef fpd, final String value, 
final Optional<MacroActions> actions) {
-        if (!fpd.isWritable()) {
-            return false;
-        }
-
-        return switch (fpd.getType()) {
-            case Enum ->
-                fpd.getEnumValues().containsKey(value);
-            case Dropdown ->
-                actions.map(a -> 
a.getDropdownValues(fpd.getKey()).containsKey(value)).orElse(false);
-            default ->
-                value != null;
-        };
-    }
-
     protected Optional<JexlContext> check(
             final SyncopeForm macroTaskForm,
             final Optional<MacroActions> actions,
@@ -112,47 +101,81 @@ public class MacroJobDelegate extends 
AbstractSchedTaskJobDelegate<MacroTask> {
             throw new JobExecutionException("Required form properties missing: 
" + missingFormProperties);
         }
 
+        // build the JEXL context where variables are mapped to property 
values, built according to the defined type
+        Map<String, Object> vars = new HashMap<>();
+        for (FormPropertyDef fpd : task.getFormPropertyDefs()) {
+            String value = 
macroTaskForm.getProperty(fpd.getKey()).map(FormProperty::getValue).orElse(null);
+            if (value == null) {
+                continue;
+            }
+
+            switch (fpd.getType()) {
+                case String -> {
+                    if (Optional.ofNullable(fpd.getStringRegEx()).
+                            map(pattern -> !pattern.matcher(value).matches()).
+                            orElse(false)) {
+
+                        throw new JobExecutionException("RegEx not matching 
for " + fpd.getKey() + ": " + value);
+                    }
+
+                    vars.put(fpd.getKey(), value);
+                }
+
+                case Password ->
+                    vars.put(fpd.getKey(), value);
+
+                case Boolean ->
+                    vars.put(fpd.getKey(), BooleanUtils.toBoolean(value));
+
+                case Date -> {
+                    try {
+                        vars.put(fpd.getKey(), 
StringUtils.isBlank(fpd.getDatePattern())
+                                ? FormatUtils.parseDate(value)
+                                : FormatUtils.parseDate(value, 
fpd.getDatePattern()));
+                    } catch (DateTimeParseException e) {
+                        throw new JobExecutionException("Unparseable date " + 
fpd.getKey() + ": " + value, e);
+                    }
+                }
+
+                case Long ->
+                    vars.put(fpd.getKey(), NumberUtils.toLong(value));
+
+                case Enum -> {
+                    if (!fpd.getEnumValues().containsKey(value)) {
+                        throw new JobExecutionException("Not allowed for " + 
fpd.getKey() + ": " + value);
+                    }
+
+                    vars.put(fpd.getKey(), value);
+                }
+
+                case Dropdown -> {
+                    if (!fpd.isDropdownFreeForm()) {
+                        List<String> values = fpd.isDropdownSingleSelection()
+                                ? List.of(value)
+                                : List.of(value.split(";"));
+
+                        if (!actions.map(a -> 
a.getDropdownValues(fpd.getKey()).keySet()).
+                                orElse(Set.of()).containsAll(values)) {
+
+                            throw new JobExecutionException("Not allowed for " 
+ fpd.getKey() + ": " + values);
+                        }
+                    }
+
+                    vars.put(fpd.getKey(), value);
+                }
+
+                default -> {
+                }
+            }
+        }
+
         // if validator is defined, validate the provided form
         try {
-            actions.ifPresent(a -> a.validate(macroTaskForm));
+            actions.ifPresent(a -> a.validate(macroTaskForm, vars));
         } catch (ValidationException e) {
             throw new JobExecutionException("Invalid form submitted for task " 
+ task.getKey(), e);
         }
 
-        // build the JEXL context where variables are mapped to property 
values, built according to the defined type
-        Map<String, Object> vars = macroTaskForm.getProperties().stream().
-                map(p -> task.getFormPropertyDefs().stream().
-                filter(fpd -> fpd.getKey().equals(p.getId()) && validate(fpd, 
p.getValue(), actions)).findFirst().
-                map(fpd -> Pair.of(fpd, p.getValue()))).
-                filter(Optional::isPresent).map(Optional::get).
-                map(pair -> {
-                    Object value;
-                    switch (pair.getLeft().getType()) {
-                        case Boolean:
-                            value = BooleanUtils.toBoolean(pair.getRight());
-                            break;
-
-                        case Date:
-                            value = 
StringUtils.isBlank(pair.getLeft().getDatePattern())
-                                    ? FormatUtils.parseDate(pair.getRight())
-                                    : FormatUtils.parseDate(pair.getRight(), 
pair.getLeft().getDatePattern());
-                            break;
-
-                        case Long:
-                            value = NumberUtils.toLong(pair.getRight());
-                            break;
-
-                        case Enum:
-                        case Dropdown:
-                        case String:
-                        case Password:
-                        default:
-                            value = pair.getRight();
-                    }
-
-                    return Pair.of(pair.getLeft().getKey(), value);
-                }).collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
-
         output.append("Form parameter values: ").append(vars).append("\n\n");
 
         return vars.isEmpty() ? Optional.empty() : Optional.of(new 
MapContext(vars));
@@ -165,10 +188,10 @@ public class MacroJobDelegate extends 
AbstractSchedTaskJobDelegate<MacroTask> {
             final boolean dryRun)
             throws JobExecutionException {
 
-        Future<AtomicReference<Pair<String, Exception>>> future = 
executor.submit(
+        Future<AtomicReference<Pair<String, Throwable>>> future = 
executor.submit(
                 new DelegatingSecurityContextCallable<>(() -> {
 
-                    AtomicReference<Pair<String, Exception>> error = new 
AtomicReference<>();
+                    AtomicReference<Pair<String, Throwable>> error = new 
AtomicReference<>();
 
                     for (int i = 0; i < commands.size() && error.get() == 
null; i++) {
                         Pair<Command<CommandArgs>, CommandArgs> command = 
commands.get(i);
@@ -187,14 +210,14 @@ public class MacroJobDelegate extends 
AbstractSchedTaskJobDelegate<MacroTask> {
 
                                 output.append(cmdOut);
                             }
-                        } catch (Exception e) {
+                        } catch (Throwable t) {
                             if (task.isContinueOnError()) {
-                                output.append("Continuing on error: 
<").append(e.getMessage()).append('>');
+                                output.append("Continuing on error: 
<").append(t.getMessage()).append('>');
 
                                 LOG.error("While running {} with args {}, 
continuing on error",
-                                        
command.getLeft().getClass().getName(), command.getRight(), e);
+                                        
command.getLeft().getClass().getName(), command.getRight(), t);
                             } else {
-                                
error.set(Pair.of(command.getLeft().getClass().getName(), e));
+                                
error.set(Pair.of(AopUtils.getTargetClass(command.getLeft()).getName(), t));
                             }
                         }
                         output.append("\n\n");
@@ -204,7 +227,7 @@ public class MacroJobDelegate extends 
AbstractSchedTaskJobDelegate<MacroTask> {
                 }));
 
         try {
-            AtomicReference<Pair<String, Exception>> error = future.get();
+            AtomicReference<Pair<String, Throwable>> error = future.get();
             if (error.get() != null) {
                 throw new JobExecutionException("While running " + 
error.get().getLeft(), error.get().getRight());
             }
diff --git 
a/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/panels/UserRequestFormPanel.java
 
b/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/panels/UserRequestFormPanel.java
index 78f64fa198..e2e80e488a 100644
--- 
a/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/panels/UserRequestFormPanel.java
+++ 
b/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/panels/UserRequestFormPanel.java
@@ -44,9 +44,7 @@ public abstract class UserRequestFormPanel extends 
SyncopeFormPanel<UserRequestF
         MetaDataRoleAuthorizationStrategy.authorize(userDetails, ENABLE, 
IdRepoEntitlement.USER_READ);
 
         boolean enabled = form.getUserTO() != null;
-        userDetails.setVisible(enabled).setEnabled(enabled);
-
-        add(userDetails);
+        add(userDetails.setVisible(enabled).setEnabled(enabled));
     }
 
     protected abstract void viewDetails(AjaxRequestTarget target);


Reply via email to