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

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 32257cb456ed7d474d7cf421e9938e2814b3c692
Author: Alex Heneveld <a...@cloudsoft.io>
AuthorDate: Tue Feb 13 14:49:04 2024 +0000

    more flexible workflow shorthand - set entity on clear/set sensor/config, 
and entity on invoke-effector
---
 .../apache/brooklyn/core/effector/Effectors.java   |  51 +++++-
 .../core/workflow/WorkflowStepDefinition.java      |  17 +-
 .../steps/appmodel/ClearConfigWorkflowStep.java    |  18 +-
 .../steps/appmodel/ClearSensorWorkflowStep.java    |  13 +-
 .../workflow/steps/appmodel/EntityValueToSet.java  |   2 +-
 .../steps/appmodel/InvokeEffectorWorkflowStep.java | 148 ++++++++++++----
 .../steps/appmodel/SetConfigWorkflowStep.java      |  12 +-
 .../steps/appmodel/SetSensorWorkflowStep.java      |  12 +-
 .../core/workflow/utils/ExpressionParser.java      |   2 +
 .../core/workflow/utils/ExpressionParserImpl.java  |   2 +-
 .../workflow/WorkflowConfigSensorEffectorTest.java | 190 +++++++++++++++++++++
 11 files changed, 407 insertions(+), 60 deletions(-)

diff --git 
a/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java 
b/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java
index 670a7b5f2e..fce5306c5b 100644
--- a/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java
+++ b/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java
@@ -18,6 +18,18 @@
  */
 package org.apache.brooklyn.core.effector;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -40,15 +52,12 @@ import org.apache.brooklyn.core.workflow.WorkflowEffector;
 import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
 import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.text.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.annotation.Nullable;
-import java.util.*;
-import java.util.function.Consumer;
-
 public class Effectors {
 
     private static final Logger log = LoggerFactory.getLogger(Effectors.class);
@@ -174,7 +183,7 @@ public class Effectors {
             if (eff2 != eff) {
                 if (eff2 instanceof EffectorWithBody) {
                     log.debug("Replacing invocation of {} on {} with {} which 
is the impl defined at that entity", new Object[] { eff, entity, eff2 });
-                    return 
((EffectorWithBody<T>)eff2).getBody().newTask(entity, eff2, 
ConfigBag.newInstance().putAll(parameters));
+                    return 
((EffectorWithBody<T>)eff2).getBody().newTask(entity, eff2, 
getConfigBagWithParametersCoerced(eff2, parameters, false));
                 } else {
                     log.warn("Effector {} defined on {} has no body; invoking 
caller-supplied {} instead", new Object[] { eff2, entity, eff });
                 }
@@ -185,14 +194,40 @@ public class Effectors {
         
         if (eff instanceof EffectorWithBody) {
             if (eff instanceof WorkflowEffector.WorkflowEffectorAndBody) {
-                return (TaskAdaptable<T>) 
((WorkflowEffector.WorkflowEffectorAndBody) 
eff).getBody().newSubWorkflowTask(entity, eff, 
ConfigBag.newInstance().putAll(parameters), parent, parentWorkflowInitializer);
+                return (TaskAdaptable<T>) 
((WorkflowEffector.WorkflowEffectorAndBody) 
eff).getBody().newSubWorkflowTask(entity, eff, 
getConfigBagWithParametersCoerced(eff, parameters, false), parent, 
parentWorkflowInitializer);
             } else {
-                return ((EffectorWithBody<T>) eff).getBody().newTask(entity, 
eff, ConfigBag.newInstance().putAll(parameters));
+                return ((EffectorWithBody<T>) eff).getBody().newTask(entity, 
eff, getConfigBagWithParametersCoerced(eff, parameters, false));
             }
         }
         
         throw new UnsupportedOperationException("No implementation registered 
for effector "+eff+" on "+entity);
-    }    
+    }
+
+    public static ConfigBag getConfigBagWithParametersCoerced(Effector<?> eff, 
@Nullable Map<?,?> map, boolean requireParameter) {
+        ConfigBag bag = ConfigBag.newInstance();
+        if (map!=null) {
+            map.forEach( (ko,v) -> {
+                String k;
+                if (ko instanceof String) k = (String) ko;
+                else if (ko instanceof ConfigKey) k = 
((ConfigKey)ko).getName();
+                else throw new IllegalArgumentException("Invalid parameter 
'"+ko+"' for effector "+eff.getName()+"; should be a string or config key");
+                Optional<ParameterType<?>> p = 
eff.getParameters().stream().filter(pi -> k.equals(pi.getName())).findFirst();
+                if (!p.isPresent()) {
+                    if (!requireParameter) {
+                        // many invocations pass ad hoc parameters
+                        bag.putStringKey(k, v);
+                    } else {
+                        // might be nice to stop doing that, but even some 
workflow eg update children on_update takes parameters it doesn't declare
+                        throw new IllegalArgumentException("No such parameter 
'" + k + "' for effector " + eff.getName());
+                    }
+                } else {
+                    Object v2 = TypeCoercions.tryCoerce(v, 
p.get().getParameterType()).orThrow("Invalid parameter value for '" + k + "' 
for effector " + eff.getName() + "; cannot convert to " + 
p.get().getParameterType());
+                    bag.putStringKey(k, v2);
+                }
+            });
+        }
+        return bag;
+    }
 
     public static <V> ParameterType<V> asParameterType(ConfigKey<V> key) {
         return key.hasDefaultValue()
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
index 34a5502996..25ca27e38c 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
@@ -164,14 +164,19 @@ public abstract class WorkflowStepDefinition {
     }
 
     protected Map<String, Object> populateFromShorthandTemplate(String 
template, String value, boolean finalMatchRaw, boolean failOnError) {
+        Map<String, Object> result = getFromShorthandTemplate(template, value, 
finalMatchRaw, failOnError);
+        if (result == null) return null;
+        input.putAll((Map<? extends String, ?>) 
CollectionMerger.builder().build().merge(input, result));
+        return result;
+    }
+
+    protected Map<String, Object> getFromShorthandTemplate(String template, 
String value, boolean finalMatchRaw, boolean failOnError) {
         Maybe<Map<String, Object>> result = new 
ShorthandProcessor(template).withFinalMatchRaw(finalMatchRaw).process(value);
-        if (result.isAbsent()) {
-            if (failOnError) throw new IllegalArgumentException("Invalid 
shorthand expression: '" + value + "'", Maybe.Absent.getException(result));
+        return result.or(() -> {
+            if (failOnError)
+                throw new IllegalArgumentException("Invalid shorthand 
expression: '" + value + "'", Maybe.Absent.getException(result));
             return null;
-        }
-
-        input.putAll((Map<? extends String, ?>) 
CollectionMerger.builder().build().merge(input, result.get()));
-        return result.get();
+        });
     }
 
     final Task<?> newTask(WorkflowStepInstanceExecutionContext context) {
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearConfigWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearConfigWorkflowStep.java
index 8b8cd500f7..af251367a9 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearConfigWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearConfigWorkflowStep.java
@@ -18,6 +18,8 @@
  */
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
+import java.util.Objects;
+
 import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.config.ConfigKey;
@@ -26,13 +28,16 @@ import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowStepResolution;
 import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.lang3.ObjectUtils;
 
 public class ClearConfigWorkflowStep extends WorkflowStepDefinition {
 
-    public static final ConfigKey<EntityValueToSet> CONFIG = 
ConfigKeys.newConfigKey(EntityValueToSet.class, "config");
+    public static final String SHORTHAND = "[ ${config.type} ] ${config.name} 
[ \" on \" ${config.entity} ]";
 
-    public static final String SHORTHAND = "[ ${config.type} ] ${config.name}";
+    public static final ConfigKey<EntityValueToSet> CONFIG = 
ConfigKeys.newConfigKey(EntityValueToSet.class, "config");
+    public static final ConfigKey<Object> ENTITY = 
ConfigKeys.newConfigKey(Object.class, "entity");
 
     @Override
     public void populateFromShorthand(String expression) {
@@ -46,8 +51,13 @@ public class ClearConfigWorkflowStep extends 
WorkflowStepDefinition {
         String configName = 
context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT,
 config.name, String.class);
         if (Strings.isBlank(configName)) throw new 
IllegalArgumentException("Config key name is required");
         TypeToken<?> type = context.lookupType(config.type, () -> 
TypeToken.of(Object.class));
-        Entity entity = config.entity;
-        if (entity==null) entity = context.getEntity();
+
+        Object entityO1 = context.getInput(ENTITY);
+        if (entityO1!=null && config.entity!=null && !Objects.equals(entityO1, 
config.entity))
+            throw new IllegalArgumentException("Cannot specify different 
entities in 'entity' and 'config.entity' when clearing config");
+        Object entityO2 = ObjectUtils.firstNonNull(config.entity, entityO1, 
context.getEntity());
+        final Entity entity = WorkflowStepResolution.findEntity(context, 
entityO2).get();
+
         
((EntityInternal)entity).config().removeKey(ConfigKeys.newConfigKey(Object.class,
 configName));
         return context.getPreviousStepOutput();
     }
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java
index e04b47b591..41b37381ea 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java
@@ -21,6 +21,7 @@ package org.apache.brooklyn.core.workflow.steps.appmodel;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import com.google.common.collect.Iterables;
 import com.google.common.reflect.TypeToken;
@@ -31,16 +32,19 @@ import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowStepResolution;
 import org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.tuple.Pair;
 
 public class ClearSensorWorkflowStep extends WorkflowStepDefinition {
 
-    public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name}";
+    public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name} 
[ \" on \" ${sensor.entity} ]";
 
     public static final ConfigKey<EntityValueToSet> SENSOR = 
ConfigKeys.newConfigKey(EntityValueToSet.class, "sensor");
+    public static final ConfigKey<Object> ENTITY = 
ConfigKeys.newConfigKey(Object.class, "entity");
 
     @Override
     public void populateFromShorthand(String expression) {
@@ -56,8 +60,11 @@ public class ClearSensorWorkflowStep extends 
WorkflowStepDefinition {
         if (sensorNameAndIndices==null) throw new 
IllegalArgumentException("Sensor name is required");
 
         TypeToken<?> type = context.lookupType(sensor.type, () -> 
TypeToken.of(Object.class));
-        Entity entity = sensor.entity;
-        if (entity==null) entity = context.getEntity();
+        Object entityO1 = context.getInput(ENTITY);
+        if (entityO1!=null && sensor.entity!=null && !Objects.equals(entityO1, 
sensor.entity))
+            throw new IllegalArgumentException("Cannot specify different 
entities in 'entity' and 'sensor.entity' when clearing sensor");
+        Object entityO2 = ObjectUtils.firstNonNull(sensor.entity, entityO1, 
context.getEntity());
+        final Entity entity = WorkflowStepResolution.findEntity(context, 
entityO2).get();
 
         // TODO use WorkflowSettingItemsUtils
         if (sensorNameAndIndices.getRight().isEmpty()) {
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/EntityValueToSet.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/EntityValueToSet.java
index c00660314e..a254320d6d 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/EntityValueToSet.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/EntityValueToSet.java
@@ -35,7 +35,7 @@ public class EntityValueToSet extends TypedValueToSet {
     }
 
     @JsonInclude(JsonInclude.Include.NON_NULL)
-    public Entity entity;
+    public Object entity;
 
     public static EntityValueToSet fromString(String name) {
         return new EntityValueToSet(name);
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/InvokeEffectorWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/InvokeEffectorWorkflowStep.java
index 3cb940e42f..25f1901524 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/InvokeEffectorWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/InvokeEffectorWorkflowStep.java
@@ -18,8 +18,12 @@
  */
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
@@ -29,38 +33,134 @@ import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.effector.Effectors;
-import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
-import org.apache.brooklyn.core.entity.EntityPredicates;
-import org.apache.brooklyn.core.mgmt.internal.AppGroupTraverser;
 import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
 import org.apache.brooklyn.core.workflow.WorkflowReplayUtils;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
-import org.apache.brooklyn.core.workflow.steps.flow.SwitchWorkflowStep;
+import org.apache.brooklyn.core.workflow.WorkflowStepResolution;
+import org.apache.brooklyn.core.workflow.utils.ExpressionParser;
+import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl;
+import 
org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.CharactersCollectingParseMode;
+import 
org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.CommonParseMode;
+import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNode;
+import 
org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNodeOrValue;
+import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseValue;
 import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.lang3.ObjectUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.util.List;
-import java.util.Map;
+import static 
org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils.makeMutableCopy;
 
 public class InvokeEffectorWorkflowStep extends WorkflowStepDefinition 
implements WorkflowStepDefinition.WorkflowStepDefinitionWithSubWorkflow {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(InvokeEffectorWorkflowStep.class);
 
-    public static final String SHORTHAND = "${effector}";
+    public static final String SHORTHAND = "${effector} [ \" on \" ${entity} ] 
[ \" with \" ${args...} ]";
 
     public static final ConfigKey<Object> ENTITY = 
ConfigKeys.newConfigKey(Object.class, "entity");
     public static final ConfigKey<String> EFFECTOR = 
ConfigKeys.newStringConfigKey("effector");
     public static final ConfigKey<Map<String,Object>> ARGS = new 
MapConfigKey.Builder<>(Object.class, "args").build();
 
     @Override
-    public void populateFromShorthand(String expression) {
-        populateFromShorthandTemplate(SHORTHAND, expression);
+    public void populateFromShorthand(String value) {
+        populateFromShorthandTemplate(SHORTHAND, value, true, true);
+    }
+    @Override
+    protected Map<String, Object> getFromShorthandTemplate(String template, 
String value, boolean finalMatchRaw, boolean failOnError) {
+        Map<String, Object> result = super.getFromShorthandTemplate(template, 
value, finalMatchRaw, failOnError);
+        if (result!=null && result.containsKey(ARGS.getName())) {
+            result = makeMutableCopy(result).get();
+            Object args = result.remove(ARGS.getName());
+            Map<String,Object> argsM = MutableMap.of();
+            if (args instanceof Map) result.put(ARGS.getName(), args);
+            else if (args instanceof String) {
+                result.put(ARGS.getName(), 
parseKeyEqualsValueExpressionStringList((String) args));
+            } else {
+                throw new IllegalArgumentException("args provided to 
invoke-effector must be a map or a comma-separated sequence of key = value 
pairs");
+            }
+        }
+        return result;
+    }
+
+    public static Map<String,String> 
parseKeyEqualsValueExpressionStringList(String args) {
+        ExpressionParserImpl ep = 
ExpressionParser.newDefaultAllowingUnquotedLiteralValues()
+                .includeGroupingBracketsAtUsualPlaces()
+                .includeAllowedTopLevelTransition(ExpressionParser.WHITESPACE)
+                .includeAllowedTopLevelTransition(new 
CharactersCollectingParseMode("equals", '='))
+                .includeAllowedTopLevelTransition(new 
CharactersCollectingParseMode("colon", ':'))
+                .includeAllowedTopLevelTransition(new 
CharactersCollectingParseMode("comma", ','));
+        ParseNode pr = ep.parse(args).get();
+        Map<String,String> result = MutableMap.of();
+
+        List<String> key = MutableList.of();
+        List<String> value = MutableList.of();
+        List<String> ws = MutableList.of();
+
+        Runnable save = () -> {
+            String old = result.put(Strings.join(key, ""), Strings.join(value, 
""));
+            if (old!=null) throw new IllegalArgumentException("Duplicate 
argument: "+Strings.join(key, ""));
+        };
+        List<String> current = key;
+        for (ParseNodeOrValue c: pr.getContents()) {
+            Maybe<String> add = null;
+            Boolean advance = null;
+
+            if (c.isParseNodeMode(ExpressionParser.WHITESPACE) ) {
+                ws.add(c.getSource());
+                continue;
+            }
+
+            if (c.isParseNodeMode(ExpressionParser.COMMON_BRACKETS) || 
c.isParseNodeMode(ExpressionParser.INTERPOLATED)) {
+                if (current==key) throw new IllegalArgumentException("Cannot 
use "+c.getParseNodeMode()+" in argument name");
+                add = Maybe.of(c.getSource());
+            } else if (ExpressionParser.isQuotedExpressionNode(c) || 
c.isParseNodeMode(ParseValue.MODE) || 
c.isParseNodeMode(ExpressionParser.BACKSLASH_ESCAPE)) {
+                add = Maybe.of(ExpressionParser.getUnquoted(c));
+            } else if (c.isParseNodeMode("equals", "colon")) {
+                if (current==value) throw new IllegalArgumentException("Cannot 
use "+c.getParseNodeMode()+" after argument value");
+                add = Maybe.absent();
+                advance = true;
+            } else if (c.isParseNodeMode("comma")) {
+                if (current==key) throw new IllegalArgumentException("Cannot 
use "+c.getParseNodeMode()+" in argument key");
+                add = Maybe.absent();
+                advance = true;
+            }
+
+            if (add==null) throw new IllegalArgumentException("Unexpected 
expression for argument: "+c.getSource());
+            if (add.isPresent()) {
+                if (!current.isEmpty()) {
+                    if (current==key) throw new 
IllegalArgumentException("Multiple words not permitted for argument name");
+                    current.addAll(ws);
+                }
+                current.add(add.get());
+            }
+            ws.clear();
+            if (Boolean.TRUE.equals(advance)) {
+                if (current==key) {
+                    if (current.isEmpty()) throw new 
IllegalArgumentException("Missing argument name");
+                    current = value;
+                } else {
+                    if (current.isEmpty()) throw new 
IllegalArgumentException("Missing argument value");
+                    current = key;
+                    save.run();
+                    key.clear();
+                    value.clear();
+                }
+            }
+        }
+
+        if (current==key) {
+            if (!current.isEmpty()) throw new 
IllegalArgumentException("Missing argument value");
+        } else if (current==value) {
+            if (current.isEmpty()) throw new IllegalArgumentException("Missing 
argument value");
+            save.run();
+        }
+        return result;
     }
 
     @Override
@@ -107,28 +207,10 @@ public class InvokeEffectorWorkflowStep extends 
WorkflowStepDefinition implement
 
     @Override
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
-        Object te = context.getInput(ENTITY);
-        if (te==null) te = context.getEntity();
-        if (te instanceof String) {
-            String desiredComponentId = (String) te;
-            List<Entity> firstGroupOfMatches = 
AppGroupTraverser.findFirstGroupOfMatches(context.getEntity(), true,
-                    
Predicates.and(EntityPredicates.configEqualTo(BrooklynConfigKeys.PLAN_ID, 
desiredComponentId), x->true)::apply);
-            if (firstGroupOfMatches.isEmpty()) {
-                firstGroupOfMatches = 
AppGroupTraverser.findFirstGroupOfMatches(context.getEntity(), true,
-                        
Predicates.and(EntityPredicates.idEqualTo(desiredComponentId), x->true)::apply);
-            }
-            if (!firstGroupOfMatches.isEmpty()) {
-                te = firstGroupOfMatches.get(0);
-            } else {
-                throw new IllegalStateException("Cannot find entity with ID 
'"+desiredComponentId+"'");
-            }
-        }
-        if (!(te instanceof Entity)) {
-            throw new IllegalStateException("Unsupported object for entity 
'"+te+"' ("+te.getClass()+")");
-        }
+        Entity entity = WorkflowStepResolution.findEntity(context, 
ObjectUtils.firstNonNull(context.getInput(ENTITY), context.getEntity()) ).get();
 
-        Effector<Object> effector = (Effector) ((Entity) 
te).getEntityType().getEffectorByName(context.getInput(EFFECTOR)).get();
-        TaskAdaptable<Object> invocation = 
Effectors.invocationPossiblySubWorkflow((Entity) te, effector, 
context.getInput(ARGS), context.getWorkflowExectionContext(), workflowTag -> {
+        Effector<Object> effector = (Effector) 
entity.getEntityType().getEffectorByName(context.getInput(EFFECTOR)).get();
+        TaskAdaptable<Object> invocation = 
Effectors.invocationPossiblySubWorkflow(entity, effector, 
context.getInput(ARGS), context.getWorkflowExectionContext(), workflowTag -> {
             WorkflowReplayUtils.setNewSubWorkflows(context, 
MutableList.of(workflowTag), workflowTag.getWorkflowId());
             // unlike nested case, no need to persist as single child workflow 
will persist themselves imminently, and if not no great shakes to recompute
         });
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java
index 73b1e9fd4c..31c4ddb3fd 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
 import java.util.List;
+import java.util.Objects;
 
 import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.entity.Entity;
@@ -26,14 +27,17 @@ import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowStepResolution;
 import org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.tuple.Pair;
 
 public class SetConfigWorkflowStep extends WorkflowStepDefinition {
 
-    public static final String SHORTHAND = "[ ${config.type} ] ${config.name} 
[ \"=\" ${value...} ]";
+    public static final String SHORTHAND = "[ ${config.type} ] ${config.name} 
[ \" on \" ${config.entity} ] [ \"=\" ${value...} ]";
 
     public static final ConfigKey<EntityValueToSet> CONFIG = 
ConfigKeys.newConfigKey(EntityValueToSet.class, "config");
+    public static final ConfigKey<Object> ENTITY = 
ConfigKeys.newConfigKey(Object.class, "entity");
     public static final ConfigKey<Object> VALUE = 
ConfigKeys.newConfigKey(Object.class, "value");
 
     @Override
@@ -52,7 +56,11 @@ public class SetConfigWorkflowStep extends 
WorkflowStepDefinition {
         // see note on type in SetSensorWorkflowStep
         TypeToken<?> type = context.lookupType(config.type, () -> 
TypeToken.of(Object.class));
         Object resolvedValue = context.getInput(VALUE.getName(), type);
-        Entity entity = config.entity!=null ? config.entity : 
context.getEntity();
+        Object entityO1 = context.getInput(ENTITY);
+        if (entityO1!=null && config.entity!=null && !Objects.equals(entityO1, 
config.entity))
+            throw new IllegalArgumentException("Cannot specify different 
entities in 'entity' and 'config.entity' when setting config");
+        Object entityO2 = ObjectUtils.firstNonNull(config.entity, entityO1, 
context.getEntity());
+        final Entity entity = WorkflowStepResolution.findEntity(context, 
entityO2).get();
 
         Pair<Object, Object> oldValues = 
WorkflowSettingItemsUtils.setAtIndex(nameAndIndices, true, (_oldValue) -> 
resolvedValue,
                 name -> entity.config().get((ConfigKey<Object>) 
ConfigKeys.newConfigKey(type, name)),
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java
index 3b0d512b52..dd570a1af6 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
@@ -33,10 +34,12 @@ import 
org.apache.brooklyn.core.resolve.jackson.WrappedValue;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowStepResolution;
 import org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils;
 import org.apache.brooklyn.util.core.predicates.DslPredicates;
 import 
org.apache.brooklyn.util.core.predicates.DslPredicates.DslEntityPredicateDefault;
 import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -47,9 +50,10 @@ public class SetSensorWorkflowStep extends 
WorkflowStepDefinition {
 
     static final boolean ALWAYS_USE_SENSOR_MODIFY = true;
 
-    public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name} 
[ \"=\" ${value...} ]";
+    public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name} 
[ \" on \" ${sensor.entity} ] [ \"=\" ${value...} ]";
 
     public static final ConfigKey<EntityValueToSet> SENSOR = 
ConfigKeys.newConfigKey(EntityValueToSet.class, "sensor");
+    public static final ConfigKey<Object> ENTITY = 
ConfigKeys.newConfigKey(Object.class, "entity");
     public static final ConfigKey<Object> VALUE = 
ConfigKeys.newConfigKey(Object.class, "value");
     public static final ConfigKey<DslPredicates.DslPredicate> REQUIRE = 
ConfigKeys.newConfigKey(DslPredicates.DslPredicate.class, "require",
             "Require a condition in order to set the value; if the condition 
is not satisfied, the sensor is not set");
@@ -86,7 +90,11 @@ public class SetSensorWorkflowStep extends 
WorkflowStepDefinition {
         if (sensorNameAndIndices==null) throw new 
IllegalArgumentException("Sensor name is required");
 
         final TypeToken<?> type = context.lookupType(sensor.type, () -> 
TypeToken.of(Object.class));
-        final Entity entity = sensor.entity!=null ? sensor.entity : 
context.getEntity();
+        Object entityO1 = context.getInput(ENTITY);
+        if (entityO1!=null && sensor.entity!=null && !Objects.equals(entityO1, 
sensor.entity))
+            throw new IllegalArgumentException("Cannot specify different 
entities in 'entity' and 'sensor.entity' when setting sensor");
+        Object entityO2 = ObjectUtils.firstNonNull(sensor.entity, entityO1, 
context.getEntity());
+        final Entity entity = WorkflowStepResolution.findEntity(context, 
entityO2).get();
 
         Supplier<Object> resolveOnceValueSupplier = Suppliers.memoize(() -> {
 //        // might need to be careful if defined type is different or more 
generic than type specified here;
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParser.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParser.java
index e3c5f53ac0..cf95a5420d 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParser.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParser.java
@@ -58,6 +58,8 @@ public abstract class ExpressionParser {
     public static final ParseMode PARENTHESES = 
CommonParseMode.transitionNested("parenthesis", "(", ")");
     public static final ParseMode CURLY_BRACES = 
CommonParseMode.transitionNested("curly_brace", "{", "}");
 
+    public static final List<ParseMode> COMMON_BRACKETS = 
MutableList.of(SQUARE_BRACKET, PARENTHESES, CURLY_BRACES).asUnmodifiable();
+
 
     private static Multimap<ParseMode,ParseMode> getCommonInnerTransitions() {
         ListMultimap<ParseMode,ParseMode> m = 
Multimaps.newListMultimap(MutableMap.of(), MutableList::of);
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParserImpl.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParserImpl.java
index 5a66648b28..2419560059 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParserImpl.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/ExpressionParserImpl.java
@@ -387,7 +387,7 @@ public class ExpressionParserImpl extends ExpressionParser {
     public ExpressionParserImpl includeGroupingBracketsAtUsualPlaces(ParseMode 
...optionalExplicitBrackets) {
         List<ParseMode> brackets = optionalExplicitBrackets==null ? 
MutableList.of() :
                 Arrays.asList(optionalExplicitBrackets).stream().filter(b -> 
b!=null).collect(Collectors.toList());
-        if (brackets.isEmpty()) brackets = MutableList.of(SQUARE_BRACKET, 
CURLY_BRACES, PARENTHESES);
+        if (brackets.isEmpty()) brackets.addAll(COMMON_BRACKETS);
 
         brackets.forEach(b -> includeAllowedTopLevelTransition(b));
         allowedTransitions.putAll(INTERPOLATED, brackets);
diff --git 
a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowConfigSensorEffectorTest.java
 
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowConfigSensorEffectorTest.java
new file mode 100644
index 0000000000..0386fd5dd9
--- /dev/null
+++ 
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowConfigSensorEffectorTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.brooklyn.core.workflow;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.google.common.collect.Iterables;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.effector.EffectorBody;
+import org.apache.brooklyn.core.effector.Effectors;
+import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.rebind.RebindOptions;
+import org.apache.brooklyn.core.mgmt.rebind.RebindTestFixture;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import 
org.apache.brooklyn.core.workflow.steps.appmodel.InvokeEffectorWorkflowStep;
+import org.apache.brooklyn.core.workflow.store.WorkflowRetentionAndExpiration;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+public class WorkflowConfigSensorEffectorTest extends 
RebindTestFixture<TestApplication> {
+
+    private static final Logger log = 
LoggerFactory.getLogger(WorkflowConfigSensorEffectorTest.class);
+
+    @Override
+    protected LocalManagementContext 
decorateOrigOrNewManagementContext(LocalManagementContext mgmt) {
+        WorkflowBasicTest.addWorkflowStepTypes(mgmt);
+        app = null; // clear this
+        
mgmt.getBrooklynProperties().put(WorkflowRetentionAndExpiration.WORKFLOW_RETENTION_DEFAULT,
 "forever");
+        return super.decorateOrigOrNewManagementContext(mgmt);
+    }
+
+    @Override
+    protected TestApplication createApp() {
+        return null;
+    }
+
+    @Override protected TestApplication rebind() throws Exception {
+        return 
rebind(RebindOptions.create().terminateOrigManagementContext(true));
+    }
+
+    TestApplication app;
+    Task<?> lastInvocation;
+
+    Object runWorkflow(List<Object> steps) throws Exception {
+        return runWorkflow(steps, null);
+    }
+    Object runWorkflow(List<Object> steps, ConfigBag extraEffectorConfig) 
throws Exception {
+        if (app==null) app = createSampleApp();
+        WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance()
+                .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow")
+                .configure(WorkflowEffector.STEPS, steps)
+                .putAll(extraEffectorConfig));
+        eff.apply(app);
+
+        lastInvocation = 
app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null);
+        return lastInvocation.getUnchecked();
+    }
+
+    TestApplication createSampleApp() {
+        return 
mgmt().getEntityManager().createEntity(EntitySpec.create(TestApplication.class)
+                
.child(EntitySpec.create(TestEntity.class).configure(BrooklynConfigKeys.PLAN_ID,
 "chilld")));
+    }
+
+    @Test
+    public void testSetAndClearSensorOnEntity() throws Exception {
+        runWorkflow(MutableList.of("set-sensor foo on chilld = bar"));
+        Entity chilld = Iterables.getOnlyElement(app.getChildren());
+        AttributeSensor<String> sensor = Sensors.newStringSensor("foo");
+        Asserts.assertEquals(chilld.sensors().get(sensor), "bar");
+        Asserts.assertNull(app.sensors().get(sensor));
+
+        runWorkflow(MutableList.of("clear-sensor foo on chilld"));
+        Asserts.assertNull(app.sensors().get(sensor));
+
+        runWorkflow(MutableList.of(MutableMap.of(
+                "s", "set-sensor",
+                "sensor", "foo",
+                "value", "bar2",
+                "entity", "chilld")
+        ));
+        Asserts.assertEquals(chilld.sensors().get(sensor), "bar2");
+
+        runWorkflow(MutableList.of(MutableMap.of(
+                "s", "set-sensor",
+                "sensor", "foo",
+                "entity", chilld,
+                "value", "bar3")
+        ));
+        Asserts.assertEquals(chilld.sensors().get(sensor), "bar3");
+
+        runWorkflow(MutableList.of(MutableMap.of(
+                "s", "set-sensor",
+                "sensor", MutableMap.of("name", "foo", "entity", "chilld"),
+                "value", "bar4")
+        ));
+        Asserts.assertEquals(chilld.sensors().get(sensor), "bar4");
+    }
+
+    @Test
+    public void testSetAndClearConfigOnEntity() throws Exception {
+        runWorkflow(MutableList.of("set-config foo on chilld = bar"));
+        Entity chilld = Iterables.getOnlyElement(app.getChildren());
+        ConfigKey<String> cfg = ConfigKeys.newStringConfigKey("foo");
+        Asserts.assertEquals(chilld.config().get(cfg), "bar");
+        Asserts.assertNull(app.config().get(cfg));
+
+        runWorkflow(MutableList.of("clear-config foo on chilld"));
+        Asserts.assertNull(app.config().get(cfg));
+    }
+
+    @Test
+    public void testParseKeyEqualsValueExpressionStringList() {
+        
Asserts.assertEquals(InvokeEffectorWorkflowStep.parseKeyEqualsValueExpressionStringList("key
 = value"),
+                MutableMap.of("key", "value"));
+        
Asserts.assertEquals(InvokeEffectorWorkflowStep.parseKeyEqualsValueExpressionStringList("key
 = value, k2 = v2"),
+                MutableMap.of("key", "value", "k2", "v2"));
+        
Asserts.assertEquals(InvokeEffectorWorkflowStep.parseKeyEqualsValueExpressionStringList("key
 = { sk = sv }, k2 : v2 v2b  ,  k3 =  \"v3 v4\"  v5 "),
+                MutableMap.of("key", "{ sk = sv }", "k2", "v2 v2b", "k3", "v3 
v4  v5"));
+    }
+
+    @Test
+    public void testInvokeEffectorOnEntity() throws Exception {
+        runWorkflow(MutableList.of(MutableMap.of(
+                "step", "invoke-effector identityEffector on chilld",
+                "args", MutableMap.of("arg", "V"))));
+        Asserts.assertEquals(lastInvocation.getUnchecked(), "V");
+
+        runWorkflow(MutableList.of(MutableMap.of("step", "invoke-effector 
identityEffector on chilld with arg = V")));
+        Asserts.assertEquals(lastInvocation.getUnchecked(), "V");
+
+        runWorkflow(MutableList.of(
+                "let map v = { key: value }",
+                MutableMap.of("step", "invoke-effector identityEffector on 
chilld with arg = ${v}")));
+        Asserts.assertEquals(lastInvocation.getUnchecked(), 
MutableMap.of("key", "value"));
+
+        runWorkflow(MutableList.of(MutableMap.of("step", "invoke-effector 
identityEffector on chilld with arg = \"{ key: value }\"")));
+        Asserts.assertEquals(lastInvocation.getUnchecked(), "{ key: value }");
+
+        // and check we can cast, with multiple arguments
+        Entity chilld = Iterables.getOnlyElement(app.getChildren());
+        
((EntityInternal)chilld).getMutableEntityType().addEffector(Effectors.effector(Object.class,
 "mapSwapped")
+                        .parameter(Map.class, "skmap")
+                        .parameter(Integer.class, "n")
+                        .impl(new MapSwapped()).build());
+        runWorkflow(MutableList.of(MutableMap.of("step", "invoke-effector 
mapSwapped on chilld with n= 2,skmap = { key: value }")));
+        Asserts.assertEquals(lastInvocation.getUnchecked(), "key=valuex2");
+    }
+
+    static class MapSwapped extends EffectorBody<Object> {
+        @Override
+        public Object call(ConfigBag p) {
+            Map skmap = (Map) p.getStringKey("skmap");
+            Integer n = (Integer) p.getStringKey("n");
+            Entry<?, ?> entry = Iterables.getOnlyElement(((Map<?, ?>) 
skmap).entrySet());
+            return ""+entry.getKey()+"="+entry.getValue()+"x"+n;
+        }
+    }
+}


Reply via email to