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; + } + } +}