more flexible workflow shorthand - set entity on clear/set sensor/config, and entity on invoke-effector
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 670a7b5..fce5306 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.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 @@
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 @@
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 34a5502..25ca27e 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 @@
}
protected Map<String, Object> populateFromShorthandTemplate(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 null;
- }
+ 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;
+ }
- input.putAll((Map<? extends String, ?>) CollectionMerger.builder().build().merge(input, result.get()));
- return result.get();
+ 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);
+ return result.or(() -> {
+ if (failOnError)
+ throw new IllegalArgumentException("Invalid shorthand expression: '" + value + "'", Maybe.Absent.getException(result));
+ return null;
+ });
}
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 8b8cd50..af25136 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.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 @@
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 e04b47b..41b3738 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 @@
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.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 @@
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 c006603..a254320 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 @@
}
@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 3cb940e..25f1901 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.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 @@
@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 73b1e9f..31c4ddb 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.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 @@
// 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 3b0d512..dd570a1 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.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 @@
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 @@
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 e3c5f53..cf95a54 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 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 5a66648..2419560 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 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 0000000..0386fd5
--- /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;
+ }
+ }
+}